@alexkroman1/aai-cli 5.13.2 → 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 (236) 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 +32 -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 +21 -30
  53. package/dist/templates/dispatch-center/agent.ts +4 -31
  54. package/dist/templates/dispatch-center/client.tsx +41 -28
  55. package/dist/templates/dispatch-center/shared.ts +73 -66
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
  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 +32 -28
  82. package/dist/templates/infocom-adventure/shared.ts +36 -16
  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 +59 -58
  102. package/dist/templates/pizza-ordering/agent.ts +10 -159
  103. package/dist/templates/pizza-ordering/client.tsx +4 -4
  104. package/dist/templates/pizza-ordering/shared.ts +44 -27
  105. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  106. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  108. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  109. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  110. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  111. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  112. package/dist/templates/plan-and-execute/agent.ts +23 -0
  113. package/dist/templates/plan-and-execute/client.tsx +115 -0
  114. package/dist/templates/plan-and-execute/graph.ts +178 -0
  115. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  116. package/dist/templates/plan-and-execute/shared.ts +161 -0
  117. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  118. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  119. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  120. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  121. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  122. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  123. package/dist/templates/recap-workflow/agent.ts +95 -0
  124. package/dist/templates/recap-workflow/shared.ts +43 -0
  125. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  126. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  127. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  128. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  129. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  130. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  131. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  132. package/dist/templates/redline/agent.test.ts +222 -0
  133. package/dist/templates/redline/agent.ts +89 -0
  134. package/dist/templates/redline/client.tsx +184 -0
  135. package/dist/templates/redline/workflows/prompts.ts +68 -0
  136. package/dist/templates/redline/workflows/redline.ts +258 -0
  137. package/dist/templates/research-workflow/agent.test.ts +525 -0
  138. package/dist/templates/research-workflow/agent.ts +94 -0
  139. package/dist/templates/research-workflow/shared.ts +35 -0
  140. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  141. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  142. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  143. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  144. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  145. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  146. package/dist/templates/retail/agent.test.ts +319 -256
  147. package/dist/templates/retail/agent.ts +6 -46
  148. package/dist/templates/retail/authenticate.ts +2 -2
  149. package/dist/templates/retail/client.tsx +45 -24
  150. package/dist/templates/retail/registry.test.ts +40 -25
  151. package/dist/templates/retail/resolve.test.ts +17 -16
  152. package/dist/templates/retail/resolve.ts +32 -67
  153. package/dist/templates/retail/shared.test.ts +21 -19
  154. package/dist/templates/retail/shared.ts +61 -23
  155. package/dist/templates/retail/store.test.ts +47 -52
  156. package/dist/templates/retail/store.ts +92 -79
  157. package/dist/templates/retail/swap.test.ts +20 -17
  158. package/dist/templates/retail/swap.ts +7 -14
  159. package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
  160. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
  161. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  162. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  163. package/dist/templates/retail/tools/get_item_details.ts +7 -8
  164. package/dist/templates/retail/tools/get_order_details.ts +7 -8
  165. package/dist/templates/retail/tools/get_product_details.ts +6 -8
  166. package/dist/templates/retail/tools/get_user_details.ts +6 -8
  167. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  168. package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
  169. package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
  170. package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
  171. package/dist/templates/retail/tools/modify_user_address.ts +6 -10
  172. package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
  173. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  174. package/dist/templates/solo-rpg/agent.test.ts +169 -54
  175. package/dist/templates/solo-rpg/agent.ts +4 -22
  176. package/dist/templates/solo-rpg/client.tsx +19 -6
  177. package/dist/templates/solo-rpg/shared.ts +20 -20
  178. package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
  179. package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
  180. package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
  181. package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
  182. package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
  183. package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
  184. package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
  185. package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
  186. package/dist/templates/support-line/agent.test.ts +343 -0
  187. package/dist/templates/support-line/agent.ts +27 -0
  188. package/dist/templates/support-line/client.tsx +122 -0
  189. package/dist/templates/support-line/graph.ts +224 -0
  190. package/dist/templates/support-line/knowledge.json +65 -0
  191. package/dist/templates/support-line/prompts.ts +95 -0
  192. package/dist/templates/support-line/shared.ts +240 -0
  193. package/dist/templates/support-line/system-prompt.md +17 -0
  194. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  195. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  196. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  197. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  198. package/dist/templates/transcription-workflow/agent.ts +92 -0
  199. package/dist/templates/transcription-workflow/client.tsx +234 -0
  200. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  201. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  202. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  203. package/dist/templates/travel-concierge/agent.ts +29 -0
  204. package/dist/templates/travel-concierge/client.tsx +161 -0
  205. package/dist/templates/travel-concierge/routing.ts +138 -0
  206. package/dist/templates/travel-concierge/shared.ts +547 -0
  207. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  208. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  209. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  210. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  211. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  212. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  213. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  214. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  215. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  216. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  217. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  218. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  219. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  220. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  221. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  222. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  223. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  224. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  225. package/dist/templates/web-researcher/agent.ts +0 -2
  226. package/dist/templates/web-researcher/system-prompt.md +10 -0
  227. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  228. package/dist/typecheck.mjs +17 -15
  229. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  230. package/dist/worker-bundler.d.ts +14 -0
  231. package/dist/worker-bundler.mjs +1 -108
  232. package/dist/workflow-B-O97AqI.mjs +125 -0
  233. package/dist/workflow-bundler.d.ts +133 -0
  234. package/dist/workflow.d.ts +67 -0
  235. package/package.json +13 -9
  236. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { checkChaosInterrupt, creativitySeed, d, getGameState, pick } from "../shared.ts";
3
+ import { checkChaosInterrupt, creativitySeed, d, gameSlot, pick } from "../shared.ts";
4
4
 
5
5
  const REACTIONS = [
6
6
  "Acts on their agenda",
@@ -202,7 +202,7 @@ const THEMES = [
202
202
  "Wound",
203
203
  ];
204
204
 
205
- export const oracle = tool({
205
+ export default tool({
206
206
  description:
207
207
  "Consult the oracle for narrative inspiration. Generates random prompts from thematic tables.",
208
208
  inputSchema: z.object({
@@ -210,7 +210,7 @@ export const oracle = tool({
210
210
  .enum(["action_theme", "npc_reaction", "scene_twist", "yes_no", "chaos_check"])
211
211
  .describe("Type of oracle consultation"),
212
212
  }),
213
- async execute(args, ctx) {
213
+ execute(args, ctx) {
214
214
  if (args.type === "yes_no") {
215
215
  const roll = d(6);
216
216
  const answer = roll <= 2 ? "No" : roll <= 4 ? "Yes, but with a complication" : "Yes";
@@ -218,15 +218,22 @@ export const oracle = tool({
218
218
  }
219
219
 
220
220
  if (args.type === "chaos_check") {
221
- // getGameState returns the live state object mutations stick.
222
- const state = getGameState(ctx);
223
- const interrupt = checkChaosInterrupt(state);
224
- return {
225
- type: "chaos_check",
226
- chaosFactor: state.chaosFactor,
227
- interrupted: Boolean(interrupt),
228
- interruptType: interrupt,
229
- };
221
+ // `checkChaosInterrupt` LOWERS the chaos factor when the roll lands, so
222
+ // this branch is a write and has to go through the slot's mutation
223
+ // window: `gameSlot.get` hands out the deep-frozen stored value, and
224
+ // assigning to it is a compile error and a `TypeError`. It is one branch
225
+ // of five, which is why this stays an ordinary `tool()` with a scoped
226
+ // `update` rather than becoming `gameSlot.updateTool` — the other four
227
+ // read nothing and store nothing.
228
+ return gameSlot.update(ctx, (game) => {
229
+ const interrupt = checkChaosInterrupt(game);
230
+ return {
231
+ type: "chaos_check",
232
+ chaosFactor: game.chaosFactor,
233
+ interrupted: Boolean(interrupt),
234
+ interruptType: interrupt,
235
+ };
236
+ });
230
237
  }
231
238
 
232
239
  if (args.type === "npc_reaction") {
@@ -1,20 +1,23 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
- import { getGameState, saveSlotKey, saveSlotParam, saveState } from "../shared.ts";
2
+ import { gameSlot, saveSlotKey, saveSlotParam, saveState } from "../shared.ts";
4
3
 
5
4
  // Requires storage — `aai storage enable` (or DATABASE_URL in .env under
6
5
  // `aai dev`); the rest of the game works without it.
7
- export const saveGame = tool({
6
+ //
7
+ // `gameSlot.tool` even though the body AWAITS: the reading half places no
8
+ // constraint on the body (only `updateTool` must be synchronous, because its
9
+ // draft is stored when it returns). What it does place is the frozen value, and
10
+ // a save is the purest read there is.
11
+ export default gameSlot.tool({
8
12
  description: "Save current game to persistent storage.",
9
13
  inputSchema: z.object({ slot: saveSlotParam }),
10
- async execute(args, ctx) {
11
- const state = getGameState(ctx);
12
- await saveState(ctx, saveSlotKey(args.slot), state);
14
+ async execute(args, game, ctx) {
15
+ await saveState(ctx, saveSlotKey(args.slot), game);
13
16
  return {
14
17
  saved: true,
15
18
  slot: args.slot ?? "autosave",
16
- name: state.playerName,
17
- scene: state.sceneCount,
19
+ name: game.playerName,
20
+ scene: game.sceneCount,
18
21
  };
19
22
  },
20
23
  });
@@ -8,17 +8,17 @@ import {
8
8
  DEFAULT_STATE,
9
9
  DISPOSITIONS,
10
10
  GENRES,
11
+ gameSlot,
11
12
  MAX_CLOCK_SEGMENTS,
12
13
  MIN_CLOCK_SEGMENTS,
13
14
  makeNpc,
14
- saveGameState,
15
15
  shuffle,
16
16
  stateSummary,
17
17
  TIME_PHASES,
18
18
  TONES,
19
19
  } from "../shared.ts";
20
20
 
21
- export const setupCharacter = tool({
21
+ export default tool({
22
22
  description:
23
23
  "Set up the entire game in one call. Starts a completely fresh game (any previous unsaved game is replaced), generates stats, initializes state, and marks the game as ready. After this returns, just narrate the opening scene. No need to call update_state — everything is already done.",
24
24
  inputSchema: z.object({
@@ -191,7 +191,7 @@ export const setupCharacter = tool({
191
191
  state.phase = "playing";
192
192
  state.sceneCount = 1;
193
193
 
194
- saveGameState(ctx, state);
194
+ gameSlot.set(ctx, state);
195
195
 
196
196
  return {
197
197
  success: true,
@@ -1,9 +1,9 @@
1
- import { tool } from "@alexkroman1/aai";
1
+ import { pushCapped } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  DEFAULT_CLOCK_SEGMENTS,
5
5
  DISPOSITIONS,
6
- getGameState,
6
+ gameSlot,
7
7
  MAX_BOND,
8
8
  MAX_CLOCK_SEGMENTS,
9
9
  MAX_CLOCKS,
@@ -19,7 +19,7 @@ import {
19
19
  updateCrisisFlags,
20
20
  } from "../shared.ts";
21
21
 
22
- export const updateState = tool({
22
+ export default gameSlot.updateTool({
23
23
  description:
24
24
  "Lightweight state sync for during gameplay. Handles location changes, NPC additions, clock additions, time changes, and session log entries. Resource changes (health/spirit/supply/momentum) are auto-applied by action_roll — only use those fields here for manual adjustments like resting or trading. Pass only what changed.",
25
25
  inputSchema: z.object({
@@ -54,8 +54,7 @@ export const updateState = tool({
54
54
  storyComplete: z.boolean().describe("Mark story as complete").optional(),
55
55
  logEntry: z.string().max(500).describe("Short log entry for this scene").optional(),
56
56
  }),
57
- async execute(args, ctx) {
58
- const state = getGameState(ctx);
57
+ execute(args, state) {
59
58
  const warnings: string[] = [];
60
59
  const clockEvents: { clock: string; trigger: string }[] = [];
61
60
 
@@ -147,14 +146,15 @@ export const updateState = tool({
147
146
 
148
147
  // Session log
149
148
  if (args.logEntry) {
150
- state.sessionLog.push({
151
- scene: state.sceneCount,
152
- summary: args.logEntry,
153
- location: state.currentLocation,
154
- });
155
- if (state.sessionLog.length > MAX_SESSION_LOG) {
156
- state.sessionLog = state.sessionLog.slice(-MAX_SESSION_LOG);
157
- }
149
+ pushCapped(
150
+ state.sessionLog,
151
+ {
152
+ scene: state.sceneCount,
153
+ summary: args.logEntry,
154
+ location: state.currentLocation,
155
+ },
156
+ MAX_SESSION_LOG,
157
+ );
158
158
  }
159
159
 
160
160
  // Crisis check
@@ -0,0 +1,343 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
4
+ import {
5
+ createToolContext,
6
+ runTool,
7
+ stubGenerate,
8
+ withDiscoveredTools,
9
+ } from "@alexkroman1/aai/testing";
10
+ import { describe, expect, test } from "vitest";
11
+ import authoredAgent from "./agent.ts";
12
+
13
+ /**
14
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
15
+ *
16
+ * The glob is written HERE rather than reached for from a shared helper because
17
+ * this file SHIPS: it is what a scaffolded project runs, so it may not import
18
+ * anything outside its own template, and `import.meta.glob` is expanded against
19
+ * the file containing it either way. This is the pattern a user writes.
20
+ */
21
+ const agentDef = withDiscoveredTools(
22
+ authoredAgent,
23
+ import.meta.glob("./tools/*.ts", { eager: true }),
24
+ );
25
+
26
+ import { MAX_ATTEMPTS, runCorrectiveRag } from "./graph.ts";
27
+ import {
28
+ ANSWER_SYSTEM,
29
+ ANSWERS_SYSTEM,
30
+ DOC_GRADER_SYSTEM,
31
+ GROUNDED_SYSTEM,
32
+ REWRITE_SYSTEM,
33
+ } from "./prompts.ts";
34
+ import { retrieve, supportSlot, supportView } from "./shared.ts";
35
+
36
+ // ─── A scripted model ────────────────────────────────────────────────────────
37
+ //
38
+ // Every node in the graph is one `ctx.generate` call, and each call is
39
+ // identified by the system prompt it carries — so a fake that switches on
40
+ // `options.system` can drive the graph down any path, deterministically, with
41
+ // no model and no network. `calls` is the transcript, which is what the routing
42
+ // assertions read: the point of these tests is WHICH nodes ran, not what a
43
+ // model would have said.
44
+
45
+ interface Script {
46
+ /** Whether the grader passes a document, per retrieval attempt (1-based). */
47
+ relevant?: (docId: string, attempt: number) => boolean;
48
+ /** What `transform_query` rewrites to. */
49
+ rewrite?: string;
50
+ /** Answers, consumed one per `generate` node. */
51
+ answers?: string[];
52
+ /** Verdicts, consumed one per grader call. Default: pass. */
53
+ grounded?: ("yes" | "no")[];
54
+ useful?: ("yes" | "no")[];
55
+ }
56
+
57
+ function scriptedModel(script: Script = {}) {
58
+ // The NODE transcript, which is this template's own and is why the routes
59
+ // below are functions: `stubGenerate` records the calls, and what these
60
+ // assertions need is the graph's node names, labelled with their verdicts.
61
+ const calls: string[] = [];
62
+ let attempt = 1;
63
+ const answers = [...(script.answers ?? [])];
64
+ const grounded = [...(script.grounded ?? [])];
65
+ const useful = [...(script.useful ?? [])];
66
+
67
+ const { generate } = stubGenerate({
68
+ [DOC_GRADER_SYSTEM]: (call) => {
69
+ const id = /\[(D\d+)\]/.exec(call.prompt)?.[1] ?? "?";
70
+ calls.push(`grade_documents:${id}`);
71
+ const pass = script.relevant ? script.relevant(id, attempt) : true;
72
+ return { object: { score: pass ? "yes" : "no", reason: `graded ${id}` } };
73
+ },
74
+ [REWRITE_SYSTEM]: () => {
75
+ calls.push("transform_query");
76
+ attempt++;
77
+ return script.rewrite ?? "rewritten query";
78
+ },
79
+ [ANSWER_SYSTEM]: () => {
80
+ calls.push("generate");
81
+ return answers.shift() ?? "The documented answer.";
82
+ },
83
+ [GROUNDED_SYSTEM]: () => {
84
+ const verdict = grounded.shift() ?? "yes";
85
+ calls.push(`grade_generation_v_documents:${verdict}`);
86
+ return { object: { score: verdict, reason: "grounded verdict" } };
87
+ },
88
+ [ANSWERS_SYSTEM]: () => {
89
+ const verdict = useful.shift() ?? "yes";
90
+ calls.push(`grade_generation_v_question:${verdict}`);
91
+ return { object: { score: verdict, reason: "usefulness verdict" } };
92
+ },
93
+ });
94
+
95
+ return { generate, calls };
96
+ }
97
+
98
+ function makeCtx(generate: GenerateFn, sessionId?: string) {
99
+ return createToolContext({ generate, ...(sessionId ? { sessionId } : {}) });
100
+ }
101
+
102
+ /** A tool by the name the model calls it by, bound to this agent. The lookup
103
+ * and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
104
+ * what is local is only which agent they run against. */
105
+ const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
106
+ runTool(agentDef, name, args, ctx);
107
+
108
+ /** Node names without the per-call suffix, for sequence assertions. */
109
+ function nodes(calls: string[]): string[] {
110
+ return calls.map((call) => call.split(":")[0] ?? call);
111
+ }
112
+
113
+ /** The same sequence with consecutive repeats collapsed — `grade_documents`
114
+ * runs once per retrieved document, and the count is `RETRIEVE_K`'s business
115
+ * rather than the routing's. */
116
+ function phases(calls: string[]): string[] {
117
+ return nodes(calls).filter((node, index, all) => node !== all[index - 1]);
118
+ }
119
+
120
+ // ─── 1. The retriever ────────────────────────────────────────────────────────
121
+
122
+ describe("retrieval (shared.ts)", () => {
123
+ test("ranks the document a caller's words actually point at", () => {
124
+ expect(retrieve("my broadband light is red")[0]?.doc.id).toBe("D1");
125
+ expect(retrieve("nobody was home for the engineer")[0]?.doc.id).toBe("D6");
126
+ });
127
+
128
+ test("and gets the neighbouring document first often enough to need a grader", () => {
129
+ // "what does it cost to leave my contract early" ranks D7 (moving home,
130
+ // which mentions the contract AND the early exit fee) above D5, the
131
+ // document that actually answers it. Both come back, which is what makes
132
+ // the run recoverable — the document grader is the node that stops the
133
+ // neighbour being spoken as the answer, and this is the retrieval it is
134
+ // grading.
135
+ const ranked = retrieve("what does it cost to leave my contract early").map(
136
+ (one) => one.doc.id,
137
+ );
138
+ expect(ranked[0]).toBe("D7");
139
+ expect(ranked).toContain("D5");
140
+ });
141
+
142
+ test("returns nothing for a query with no content words", () => {
143
+ // Every term is a stopword, so there is nothing to rank — which the graph
144
+ // treats as a failed retrieval rather than as an empty knowledge base.
145
+ expect(retrieve("what about the one that you have")).toEqual([]);
146
+ });
147
+
148
+ test("a question phrased the way a caller speaks misses, which is the case the rewrite exists for", () => {
149
+ // The bait for the whole template: no document contains "dying", so lexical
150
+ // retrieval cannot reach D10 until `transform_query` supplies the words a
151
+ // document about it would use.
152
+ const spoken = retrieve("my internet keeps dying at night").map((one) => one.doc.id);
153
+ expect(spoken).not.toContain("D10");
154
+ expect(retrieve("evening slowdown congestion peak time")[0]?.doc.id).toBe("D10");
155
+ });
156
+ });
157
+
158
+ // ─── 2. The graph's routing ──────────────────────────────────────────────────
159
+
160
+ describe("corrective-RAG graph", () => {
161
+ test("the straight path grades, answers, and checks the answer twice", async () => {
162
+ const { generate, calls } = scriptedModel({
163
+ relevant: (id) => id === "D2",
164
+ answers: ["Power the hub off at the wall for thirty seconds."],
165
+ });
166
+ const trace = await runCorrectiveRag(generate, "how do I reboot the hub");
167
+
168
+ expect(phases(calls)).toEqual([
169
+ "grade_documents",
170
+ "generate",
171
+ "grade_generation_v_documents",
172
+ "grade_generation_v_question",
173
+ ]);
174
+ expect(trace.answer).toContain("thirty seconds");
175
+ expect(trace.grounded).toBe(true);
176
+ expect(trace.useful).toBe(true);
177
+ expect(trace.rewrites).toBe(0);
178
+ expect(trace.exhausted).toBe(false);
179
+ expect(trace.docs.filter((doc) => doc.relevant).map((doc) => doc.id)).toEqual(["D2"]);
180
+ });
181
+
182
+ test("nothing relevant rewrites the query and retries — the corrective edge", async () => {
183
+ const { generate, calls } = scriptedModel({
184
+ // First retrieval is graded useless; after the rewrite everything passes.
185
+ relevant: (_id, attempt) => attempt > 1,
186
+ rewrite: "evening slowdown congestion peak time",
187
+ answers: ["There is no traffic management, so evening slowdowns are local."],
188
+ });
189
+ const trace = await runCorrectiveRag(generate, "my internet keeps dying at night");
190
+
191
+ expect(calls.filter((call) => call === "transform_query")).toHaveLength(1);
192
+ expect(trace.rewrites).toBe(1);
193
+ expect(trace.query).toBe("evening slowdown congestion peak time");
194
+ expect(trace.answer).toContain("traffic management");
195
+ // The trace names the nodes the way their graph does, which is what the
196
+ // sidebar renders.
197
+ expect(trace.steps.map((entry) => entry.node)).toContain("transform_query");
198
+ });
199
+
200
+ test("a caller question nothing covers ends exhausted, with no answer", async () => {
201
+ const { generate, calls } = scriptedModel({ relevant: () => false });
202
+ const trace = await runCorrectiveRag(generate, "can I get a landline installed");
203
+
204
+ expect(trace.answer).toBeNull();
205
+ expect(trace.exhausted).toBe(true);
206
+ expect(trace.rewrites).toBe(MAX_ATTEMPTS - 1);
207
+ // It never reached the answer node: nothing was graded relevant to answer
208
+ // FROM, which is the whole point of grading before generating.
209
+ expect(nodes(calls)).not.toContain("generate");
210
+ });
211
+
212
+ test("an ungrounded answer is regenerated once and then withheld", async () => {
213
+ const { generate, calls } = scriptedModel({
214
+ relevant: (id) => id === "D5",
215
+ answers: ["The exit fee is nine months.", "The exit fee is still nine months."],
216
+ grounded: ["no", "no"],
217
+ });
218
+ const trace = await runCorrectiveRag(generate, "what is the early exit fee");
219
+
220
+ expect(calls.filter((call) => call === "generate")).toHaveLength(2);
221
+ // Withheld rather than spoken with a caveat: an answer the grader says is
222
+ // not in the documents is the one thing a support line must never say.
223
+ expect(trace.answer).toBeNull();
224
+ expect(trace.grounded).toBe(false);
225
+ expect(trace.exhausted).toBe(true);
226
+ expect(nodes(calls)).not.toContain("grade_generation_v_question");
227
+ });
228
+
229
+ test("a grounded but beside-the-point answer rewrites once, then is returned with its verdict", async () => {
230
+ const { generate } = scriptedModel({
231
+ relevant: () => true,
232
+ useful: ["no", "no"],
233
+ // The rewrite has to retrieve something, or the second attempt ends in
234
+ // the no-documents branch instead of the one under test.
235
+ rewrite: "direct debit bill payment date",
236
+ answers: ["Bills go out on the first.", "Bills go out on the first."],
237
+ });
238
+ const trace = await runCorrectiveRag(generate, "when will my money actually leave my account");
239
+
240
+ expect(trace.rewrites).toBe(1);
241
+ expect(trace.useful).toBe(false);
242
+ expect(trace.exhausted).toBe(true);
243
+ // Still returned: it is grounded, and the tool tells the model to offer a
244
+ // ticket alongside it rather than to withhold it.
245
+ expect(trace.answer).toBe("Bills go out on the first.");
246
+ });
247
+ });
248
+
249
+ // ─── 3. The tools ────────────────────────────────────────────────────────────
250
+
251
+ describe("answer_question", () => {
252
+ test("hands back the answer, its sources and its verdicts, and records the trace", async () => {
253
+ const { generate } = scriptedModel({
254
+ relevant: (id) => id === "D8",
255
+ answers: ["Area outages are on the status page, and rebooting will not help."],
256
+ });
257
+ const ctx = makeCtx(generate);
258
+ const result = (await run("answer_question", { question: "is there an outage" }, ctx)) as {
259
+ answer: string;
260
+ sources: string[];
261
+ grounded: boolean;
262
+ answersTheQuestion: boolean;
263
+ guidance?: string;
264
+ };
265
+
266
+ expect(result.answer).toContain("status page");
267
+ expect(result.sources).toEqual(["Checking for an outage in your area"]);
268
+ expect(result.grounded).toBe(true);
269
+ expect(result.answersTheQuestion).toBe(true);
270
+ expect(result.guidance).toBeUndefined();
271
+
272
+ const state = supportSlot.get(ctx);
273
+ expect(state.asked).toEqual(["is there an outage"]);
274
+ expect(state.trace?.answer).toBe(result.answer);
275
+ });
276
+
277
+ test("with nothing grounded it returns no answer and points at the ticket", async () => {
278
+ const { generate } = scriptedModel({ relevant: () => false });
279
+ const ctx = makeCtx(generate);
280
+ const result = (await run("answer_question", { question: "do you sell phones" }, ctx)) as {
281
+ answer: null;
282
+ guidance: string;
283
+ };
284
+ expect(result.answer).toBeNull();
285
+ expect(result.guidance).toContain("log_ticket");
286
+ });
287
+
288
+ test("a broken model call is reported rather than thrown at the turn", async () => {
289
+ // ctx.generate rejecting is the default `createToolContext` gives — a bad
290
+ // key in production looks the same from here.
291
+ const ctx = createToolContext({});
292
+ const result = (await run("answer_question", { question: "anything" }, ctx)) as {
293
+ error: string;
294
+ };
295
+ expect(result.error).toContain("knowledge base lookup failed");
296
+ });
297
+
298
+ test("two independent contexts never see each other's traces or tickets", async () => {
299
+ // What this really checks: the state lives in the SLOT and not in a
300
+ // module-level variable. `createToolContext()` hands each call its own
301
+ // detached slot store, so the isolation is per CONTEXT — two distinct
302
+ // session ids would prove nothing extra, and `sessionSlot` could stop
303
+ // keying by session with this still passing.
304
+ const { generate } = scriptedModel({ relevant: (id) => id === "D2", answers: ["Reboot it."] });
305
+ const first = makeCtx(generate);
306
+ const second = makeCtx(generate);
307
+
308
+ await run("answer_question", { question: "how do I reboot" }, first);
309
+ expect(supportSlot.get(second).trace).toBeNull();
310
+ expect(supportSlot.get(first).trace).not.toBeNull();
311
+ });
312
+ });
313
+
314
+ describe("log_ticket", () => {
315
+ test("logs a reference and keeps the callback number off the wire", async () => {
316
+ const ctx = createToolContext({});
317
+ const logged = (await run(
318
+ "log_ticket",
319
+ { question: "landline install", callback: "07700 900123" },
320
+ ctx,
321
+ )) as { reference: string };
322
+ expect(logged.reference).toBe("TCK4001");
323
+
324
+ const state = supportSlot.get(ctx);
325
+ expect(state.ticket?.callback).toBe("07700 900123");
326
+ // The projection is the privacy boundary — only the reference crosses.
327
+ const view = supportView(state);
328
+ expect(view.ticket).toBe("TCK4001");
329
+ expect(JSON.stringify(view)).not.toContain("900123");
330
+ });
331
+ });
332
+
333
+ describe("supportView projection", () => {
334
+ test("an untouched call projects an empty trace, not undefined", () => {
335
+ // Exactly the value client.tsx hoists as its fallback.
336
+ expect(supportSlot.projection(supportView)(undefined)).toMatchObject({
337
+ product: "Meridian Fibre",
338
+ trace: null,
339
+ asked: [],
340
+ ticket: null,
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,27 @@
1
+ import { agent } from "@alexkroman1/aai";
2
+ import { PRODUCT, supportSlot, supportView } from "./shared.ts";
3
+
4
+ /**
5
+ * A support line that grades its own retrieval before it speaks — the
6
+ * self-RAG / CRAG graph, ported to voice. `graph.ts` holds the loop, `prompts.ts`
7
+ * the attribution and the graders, `shared.ts` the knowledge base and why its
8
+ * retriever is lexical.
9
+ *
10
+ * **Why this shape is worth the model calls on a phone line.** A naive RAG voice
11
+ * agent retrieves, stuffs the documents into the prompt and speaks — and when
12
+ * retrieval missed, it answers from the nearest document it was handed, which
13
+ * over a phone is indistinguishable from a confident correct answer. The
14
+ * template's own knowledge base is built to bait exactly that: "cancelling your
15
+ * contract" and "cancelling an engineer visit" are two documents, two fees, and
16
+ * one word apart. The document grader is what makes the second one not get
17
+ * spoken as the answer to the first.
18
+ */
19
+ export default agent({
20
+ name: `${PRODUCT} Support`,
21
+ // The trace exists before the first tool call, so a resumed connection has
22
+ // something to project.
23
+ // The projection is also the privacy boundary: a logged ticket carries the
24
+ // caller's callback number, and only its reference crosses to the browser.
25
+ syncState: supportSlot.projection(supportView),
26
+ greeting: `${PRODUCT} support, you're through to the automated line. What's happened?`,
27
+ });
@@ -0,0 +1,122 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import { AutoScroll, client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
3
+ import type { SupportView } from "./shared.ts";
4
+ import { PRODUCT, supportSlot, supportView } from "./shared.ts";
5
+
6
+ const EMPTY_SUPPORT: SupportView = supportSlot.projection(supportView)(undefined);
7
+
8
+ /**
9
+ * The graph, as it ran for the last question.
10
+ *
11
+ * This is the panel worth having: the whole argument for the corrective loop is
12
+ * work the caller never hears — four documents retrieved, three rejected, the
13
+ * question rewritten once — and a support line whose grading is invisible is a
14
+ * support line nobody can tell apart from one that just guesses well.
15
+ */
16
+ function TraceSidebar() {
17
+ const theme = useTheme();
18
+ const support = useAgentState<SupportView>(EMPTY_SUPPORT);
19
+ const trace = support.trace;
20
+
21
+ return (
22
+ <div className="flex h-full min-h-0 flex-col gap-4 p-4" style={{ color: theme.text }}>
23
+ <div>
24
+ <h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
25
+ <p className="text-xs opacity-50">
26
+ {support.asked.length} question{support.asked.length === 1 ? "" : "s"} this call
27
+ {support.ticket ? ` · ticket ${support.ticket}` : ""}
28
+ </p>
29
+ </div>
30
+
31
+ {!trace && (
32
+ <p className="text-sm opacity-50">
33
+ Ask a question and the retrieval, the grades and the verdicts appear here.
34
+ </p>
35
+ )}
36
+
37
+ {trace && (
38
+ <AutoScroll
39
+ scrollClassName="min-h-0 overflow-y-auto"
40
+ contentClassName="flex flex-col gap-4 pr-1"
41
+ >
42
+ <div className="rounded-lg p-3" style={{ background: theme.surface }}>
43
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Question</p>
44
+ <p className="text-sm">{trace.question}</p>
45
+ {trace.rewrites > 0 && (
46
+ <p className="mt-1 text-xs opacity-60">
47
+ rewritten to <span className="italic">{trace.query}</span>
48
+ </p>
49
+ )}
50
+ </div>
51
+
52
+ <div className="flex flex-col gap-1.5">
53
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
54
+ Retrieved &amp; graded
55
+ </p>
56
+ {trace.docs.length === 0 && <p className="text-xs opacity-50">Nothing retrieved.</p>}
57
+ {trace.docs.map((doc) => (
58
+ <div
59
+ key={doc.id}
60
+ className="flex items-start gap-2 rounded-lg p-2"
61
+ style={{ background: theme.surface, opacity: doc.relevant ? 1 : 0.5 }}
62
+ >
63
+ <span style={{ color: doc.relevant ? theme.primary : theme.text }}>
64
+ {doc.relevant ? "✓" : "×"}
65
+ </span>
66
+ <div className="min-w-0">
67
+ <p className="text-sm">{doc.title}</p>
68
+ <p className="text-xs opacity-60">{doc.reason}</p>
69
+ </div>
70
+ </div>
71
+ ))}
72
+ </div>
73
+
74
+ <div className="flex flex-col gap-1">
75
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Graph</p>
76
+ {trace.steps.map((entry, index) => (
77
+ // Steps are append-only within a run, so the index is stable.
78
+ <p key={`${index}-${entry.node}`} className="text-xs">
79
+ <span className="font-mono opacity-80">{entry.node}</span>
80
+ <span className="opacity-50"> — {entry.detail}</span>
81
+ </p>
82
+ ))}
83
+ </div>
84
+
85
+ <div className="rounded-lg p-3" style={{ background: theme.surface }}>
86
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Verdict</p>
87
+ {trace.answer ? (
88
+ <p className="text-sm">{trace.answer}</p>
89
+ ) : (
90
+ <p className="text-sm opacity-60">No answer could be grounded.</p>
91
+ )}
92
+ <p className="mt-2 text-xs opacity-60">
93
+ grounded: {String(trace.grounded)} · answers the question: {String(trace.useful)}
94
+ {trace.exhausted ? " · budget exhausted" : ""}
95
+ </p>
96
+ </div>
97
+ </AutoScroll>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ client({
104
+ // Derived, not typed twice: `PRODUCT` comes off `knowledge.json`, which is
105
+ // what `agent.ts` names the agent and greets with. A knowledge base swapped
106
+ // for another product otherwise leaves the browser tab advertising the old
107
+ // one.
108
+ name: `${PRODUCT} Support`,
109
+ sidebar: TraceSidebar,
110
+ theme: {
111
+ bg: "#0b1220",
112
+ primary: "#4ea8de",
113
+ text: "#e8eef6",
114
+ surface: "#141d2e",
115
+ border: "#1f2b3f",
116
+ },
117
+ tools: {
118
+ answer_question: { icon: "\u{1F50D}", label: "Checking the knowledge base" },
119
+ list_topics: { icon: "\u{1F4DA}", label: "Listing topics" },
120
+ log_ticket: { icon: "\u{1F4DD}", label: "Logging a ticket" },
121
+ },
122
+ });