@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
@@ -0,0 +1,205 @@
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
+
8
+ /**
9
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
10
+ *
11
+ * The glob is written HERE rather than reached for from a shared helper because
12
+ * this file SHIPS: it is what a scaffolded project runs, so it may not import
13
+ * anything outside its own template, and `import.meta.glob` is expanded against
14
+ * the file containing it either way. This is the pattern a user writes.
15
+ */
16
+ const agentDef = withDiscoveredTools(
17
+ authoredAgent,
18
+ import.meta.glob("./tools/*.ts", { eager: true }),
19
+ );
20
+
21
+ import { DEFAULT_GAME_STATE, gameSlot, MAX_HISTORY, REPORTED_HISTORY } from "./shared.ts";
22
+
23
+ /** A tool by the name the model calls it by, bound to this agent. */
24
+ const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
25
+ runTool(agentDef, name, args, ctx);
26
+
27
+ /** Each context owns its OWN slot store, which is what makes two playthroughs
28
+ * independent by construction. */
29
+ const makeCtx = () => createToolContext();
30
+
31
+ // ─── The frozen-vs-draft contract ────────────────────────────────────────────
32
+ //
33
+ // This whole block exists because the template shipped without it and two tools
34
+ // were wrong. `game_state_take` and `game_state_flag` were declared with
35
+ // `gameSlot.tool` — the READING half, whose value is deep-frozen — while
36
+ // pushing to `inventory` and writing into `flags`. Every call threw a
37
+ // `TypeError`, and nothing in the repo executed either body. The declaration is
38
+ // the fix; these are what keep it fixed.
39
+
40
+ describe("the mutating tools actually mutate", () => {
41
+ test("game_state_take adds to the inventory and does not double an item", async () => {
42
+ const ctx = makeCtx();
43
+
44
+ const first = (await run("game_state_take", { value: "lantern" }, ctx)) as {
45
+ inventory: string[];
46
+ };
47
+ expect(first.inventory).toEqual(["lantern"]);
48
+ // The stored value, not the one the body returned — a body handed a frozen
49
+ // value would have thrown, and a body handed a copy nothing stores would
50
+ // report success here and leave the slot empty.
51
+ expect(gameSlot.get(ctx).inventory).toEqual(["lantern"]);
52
+
53
+ const again = (await run("game_state_take", { value: "lantern" }, ctx)) as {
54
+ inventory: string[];
55
+ };
56
+ expect(again.inventory).toEqual(["lantern"]);
57
+ });
58
+
59
+ test("game_state_flag records a flag, and a second flag joins the first", async () => {
60
+ const ctx = makeCtx();
61
+
62
+ await run("game_state_flag", { value: "gate_opened" }, ctx);
63
+ const both = (await run("game_state_flag", { value: "rope_cut" }, ctx)) as {
64
+ flags: Record<string, boolean>;
65
+ };
66
+
67
+ expect(both.flags).toEqual({ gate_opened: true, rope_cut: true });
68
+ expect(gameSlot.get(ctx).flags).toEqual({ gate_opened: true, rope_cut: true });
69
+ });
70
+
71
+ test("what a READ is handed is frozen, which is why the two above are updateTool", async () => {
72
+ const ctx = makeCtx();
73
+ await run("game_state_take", { value: "lantern" }, ctx);
74
+
75
+ const game = gameSlot.get(ctx);
76
+ // The type refuses this too (`readonly string[]` has no `push`), and the
77
+ // freeze is what makes the refusal true for a caller with no types.
78
+ expect(Object.isFrozen(game.inventory)).toBe(true);
79
+ expect(() => (game.inventory as string[]).push("sword")).toThrow(TypeError);
80
+ });
81
+ });
82
+
83
+ // ─── The rest of the eight ───────────────────────────────────────────────────
84
+
85
+ describe("the adventure's tools", () => {
86
+ test("drop removes an item and is a no-op for one the player never took", async () => {
87
+ const ctx = makeCtx();
88
+ await run("game_state_take", { value: "lantern" }, ctx);
89
+ await run("game_state_take", { value: "rope" }, ctx);
90
+
91
+ const dropped = (await run("game_state_drop", { value: "lantern" }, ctx)) as {
92
+ inventory: string[];
93
+ };
94
+ expect(dropped.inventory).toEqual(["rope"]);
95
+
96
+ const nothing = (await run("game_state_drop", { value: "sword" }, ctx)) as {
97
+ inventory: string[];
98
+ };
99
+ expect(nothing.inventory).toEqual(["rope"]);
100
+ });
101
+
102
+ test("move sets the room and counts the move", async () => {
103
+ const ctx = makeCtx();
104
+ const moved = (await run("game_state_move", { value: "Echo Chamber" }, ctx)) as {
105
+ currentRoom: string;
106
+ moves: number;
107
+ };
108
+ expect(moved).toEqual({ currentRoom: "Echo Chamber", moves: 1 });
109
+ expect(gameSlot.get(ctx).currentRoom).toBe("Echo Chamber");
110
+ });
111
+
112
+ test("score accumulates rather than replacing", async () => {
113
+ const ctx = makeCtx();
114
+ await run("game_state_score", { value: 10 }, ctx);
115
+ const total = (await run("game_state_score", { value: 5 }, ctx)) as { score: number };
116
+ expect(total.score).toBe(15);
117
+ });
118
+
119
+ test("history logs the command, counts the move, and reports only the recent ones", async () => {
120
+ const ctx = makeCtx();
121
+ for (let i = 1; i <= REPORTED_HISTORY + 2; i++) {
122
+ await run("game_state_history", { value: `command ${i}` }, ctx);
123
+ }
124
+
125
+ const last = (await run("game_state_history", { value: "look" }, ctx)) as {
126
+ moves: number;
127
+ recentHistory: string[];
128
+ };
129
+ expect(last.moves).toBe(REPORTED_HISTORY + 3);
130
+ expect(last.recentHistory).toHaveLength(REPORTED_HISTORY);
131
+ expect(last.recentHistory.at(-1)).toBe("look");
132
+ });
133
+
134
+ test("the history is capped, so a long playthrough does not grow without bound", async () => {
135
+ const ctx = makeCtx();
136
+ for (let i = 0; i < MAX_HISTORY + 10; i++) {
137
+ await run("game_state_history", { value: `command ${i}` }, ctx);
138
+ }
139
+ const game = gameSlot.get(ctx);
140
+ expect(game.history).toHaveLength(MAX_HISTORY);
141
+ // The cap drops the OLDEST — the newest command is the one a narrator needs.
142
+ expect(game.history.at(-1)).toBe(`command ${MAX_HISTORY + 9}`);
143
+ expect(game.moves).toBe(MAX_HISTORY + 10);
144
+ });
145
+
146
+ test("get reports the whole board, with the history trimmed to what a narrator reads", async () => {
147
+ const ctx = makeCtx();
148
+ await run("game_state_take", { value: "lantern" }, ctx);
149
+ await run("game_state_flag", { value: "gate_opened" }, ctx);
150
+ await run("game_state_move", { value: "Echo Chamber" }, ctx);
151
+ await run("game_state_score", { value: 7 }, ctx);
152
+ for (let i = 0; i < REPORTED_HISTORY + 3; i++) {
153
+ await run("game_state_history", { value: `command ${i}` }, ctx);
154
+ }
155
+
156
+ expect(await run("game_state_get", {}, ctx)).toEqual({
157
+ currentRoom: "Echo Chamber",
158
+ inventory: ["lantern"],
159
+ score: 7,
160
+ moves: REPORTED_HISTORY + 4,
161
+ flags: { gate_opened: true },
162
+ recentHistory: Array.from({ length: REPORTED_HISTORY }, (_, i) => `command ${i + 3}`),
163
+ });
164
+ });
165
+
166
+ test("restart replaces the whole game, and the fresh room is the one the greeting describes", async () => {
167
+ const ctx = makeCtx();
168
+ await run("game_state_take", { value: "lantern" }, ctx);
169
+ await run("game_state_score", { value: 30 }, ctx);
170
+ await run("game_state_move", { value: "Echo Chamber" }, ctx);
171
+
172
+ const restarted = (await run("game_state_restart", {}, ctx)) as {
173
+ restarted: boolean;
174
+ currentRoom: string;
175
+ };
176
+ expect(restarted).toEqual({ restarted: true, currentRoom: DEFAULT_GAME_STATE.currentRoom });
177
+ expect(gameSlot.get(ctx)).toEqual(DEFAULT_GAME_STATE);
178
+ });
179
+ });
180
+
181
+ describe("the game is per context", () => {
182
+ test("a second playthrough starts empty and cannot see the first", async () => {
183
+ // What this really checks: the state lives in the SLOT and not in a
184
+ // module-level variable. `createToolContext()` hands each call its own
185
+ // detached slot store, so the isolation is per CONTEXT — two distinct
186
+ // session ids would prove nothing extra, and `sessionSlot` could stop
187
+ // keying by session with this still passing.
188
+ const one = makeCtx();
189
+ const two = makeCtx();
190
+
191
+ await run("game_state_take", { value: "lantern" }, one);
192
+ await run("game_state_move", { value: "Echo Chamber" }, one);
193
+
194
+ expect(gameSlot.get(two)).toEqual(DEFAULT_GAME_STATE);
195
+ await run("game_state_take", { value: "rope" }, two);
196
+ expect(gameSlot.get(one).inventory).toEqual(["lantern"]);
197
+ expect(gameSlot.get(two).inventory).toEqual(["rope"]);
198
+ });
199
+
200
+ test("the module-level default is cloned, so no session can edit the next one's start", async () => {
201
+ const ctx = makeCtx();
202
+ await run("game_state_take", { value: "lantern" }, ctx);
203
+ expect(DEFAULT_GAME_STATE.inventory).toEqual([]);
204
+ });
205
+ });
@@ -1,117 +1,14 @@
1
- import { agent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import { gameSlot, recordCommand } from "./shared.ts";
4
- import systemPrompt from "./system-prompt.md?raw";
1
+ import { agent } from "@alexkroman1/aai";
5
2
 
6
3
  export default agent({
7
4
  name: "Cavern Adventure",
5
+ // The world exists before the first command, so a resumed connection has
6
+ // something to project rather than an empty state object.
8
7
  // A narrator wants a narrative voice; everything else stays on the
9
8
  // default all-AssemblyAI pipeline.
10
9
  voice: "paul",
11
- systemPrompt,
12
10
  // The opening scene here must agree with DEFAULT_GAME_STATE.currentRoom
13
11
  // (shared.ts) and the world map in system-prompt.md.
14
12
  greeting:
15
13
  "Welcome, adventurer. You are standing at the mouth of a weathered cave at the edge of a pine forest. A cold wind carries the smell of damp stone up from the darkness below. A rusted lantern hangs from an iron hook beside the entrance. What would you like to do?",
16
-
17
- tools: {
18
- game_state_drop: tool({
19
- description: "Remove an item from the player's inventory.",
20
- inputSchema: z.object({
21
- value: z.string().describe("Item name to drop"),
22
- }),
23
- async execute(args, ctx) {
24
- const g = gameSlot.get(ctx);
25
- g.inventory = g.inventory.filter((i) => i !== args.value);
26
- return { inventory: g.inventory };
27
- },
28
- }),
29
-
30
- game_state_flag: tool({
31
- description: "Set a game flag to true, used for tracking puzzle and event state.",
32
- inputSchema: z.object({
33
- value: z.string().describe("Flag name to set"),
34
- }),
35
- async execute(args, ctx) {
36
- const g = gameSlot.get(ctx);
37
- g.flags[args.value] = true;
38
- return { flags: g.flags };
39
- },
40
- }),
41
-
42
- game_state_get: tool({
43
- description:
44
- "Read the current game state including inventory, current room, score, moves, flags, and recent history.",
45
- async execute(_args, ctx) {
46
- const g = gameSlot.get(ctx);
47
- return {
48
- currentRoom: g.currentRoom,
49
- inventory: g.inventory,
50
- score: g.score,
51
- moves: g.moves,
52
- flags: g.flags,
53
- recentHistory: g.history.slice(-5),
54
- };
55
- },
56
- }),
57
-
58
- game_state_history: tool({
59
- description: "Log a player command to the history and increment the move counter.",
60
- inputSchema: z.object({
61
- value: z.string().describe("Command text to log"),
62
- }),
63
- async execute(args, ctx) {
64
- const g = gameSlot.get(ctx);
65
- recordCommand(g, args.value);
66
- g.moves++;
67
- return { moves: g.moves, recentHistory: g.history.slice(-5) };
68
- },
69
- }),
70
-
71
- game_state_move: tool({
72
- description: "Move the player to a new room and increment the move counter.",
73
- inputSchema: z.object({
74
- value: z.string().describe("Room name to move to"),
75
- }),
76
- async execute(args, ctx) {
77
- const g = gameSlot.get(ctx);
78
- g.currentRoom = args.value;
79
- g.moves++;
80
- return { currentRoom: g.currentRoom, moves: g.moves };
81
- },
82
- }),
83
-
84
- game_state_restart: tool({
85
- description:
86
- "Reset the game to the beginning: empty inventory, zero score and moves, all flags cleared. Use when the player asks to restart, quit, or start a new game.",
87
- async execute(_args, ctx) {
88
- const g = gameSlot.reset(ctx);
89
- return { restarted: true, currentRoom: g.currentRoom };
90
- },
91
- }),
92
-
93
- game_state_score: tool({
94
- description: "Add points to the player's score.",
95
- inputSchema: z.object({
96
- value: z.number().describe("Points to add"),
97
- }),
98
- async execute(args, ctx) {
99
- const g = gameSlot.get(ctx);
100
- g.score += args.value;
101
- return { score: g.score };
102
- },
103
- }),
104
-
105
- game_state_take: tool({
106
- description: "Add an item to the player's inventory.",
107
- inputSchema: z.object({
108
- value: z.string().describe("Item name to take"),
109
- }),
110
- async execute(args, ctx) {
111
- const g = gameSlot.get(ctx);
112
- if (!g.inventory.includes(args.value)) g.inventory.push(args.value);
113
- return { inventory: g.inventory };
114
- },
115
- }),
116
- },
117
14
  });
@@ -1,6 +1,6 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { ChatMessage } from "@alexkroman1/aai-ui";
3
- import { AutoScroll, client, useSession } from "@alexkroman1/aai-ui";
2
+ import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
3
+ import { AutoScroll, client, useSession, useUserTranscript } from "@alexkroman1/aai-ui";
4
4
 
5
5
  const CSS = `
6
6
  @keyframes ic-flicker {
@@ -52,8 +52,31 @@ const GREEN_DIM = "#00aa2a";
52
52
  const GREEN_DARK = "#003300";
53
53
  const CYAN = "#00ccff";
54
54
 
55
+ // The cursor dot's colour per session state, as an EXHAUSTIVE map rather than an
56
+ // if-chain with a grey default.
57
+ //
58
+ // The palette is this template's own — every client here paints the same six
59
+ // states in its own colours, so the SDK has nothing to share but the union. What
60
+ // the SDK does own is `AgentState`, and `satisfies Record<AgentState, string>`
61
+ // is what borrows it: a state added there stops compiling here, where the
62
+ // `state === "…"` chain this replaced answered a new state with a silent grey
63
+ // badge in three separate files and no way to notice.
64
+ const STATE_COLORS = {
65
+ disconnected: GREEN_DARK,
66
+ connecting: GREEN_DARK,
67
+ ready: GREEN_DARK,
68
+ listening: GREEN,
69
+ thinking: CYAN,
70
+ speaking: "#ffaa00",
71
+ error: GREEN_DARK,
72
+ } satisfies Record<AgentState, string>;
73
+
55
74
  function InfocomAdventure() {
56
75
  const session = useSession();
76
+ // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
77
+ // is silence and `""` is speech detected with no words back yet, and reading
78
+ // them as one falsy value hides the indicator at the start of every turn.
79
+ const { speaking, text: userTranscript } = useUserTranscript();
57
80
 
58
81
  const stateLabel =
59
82
  session.state === "listening"
@@ -70,14 +93,7 @@ function InfocomAdventure() {
70
93
 
71
94
  const msgCount = session.messages.filter((m: ChatMessage) => m.role === "user").length;
72
95
 
73
- const dotColor =
74
- session.state === "listening"
75
- ? GREEN
76
- : session.state === "speaking"
77
- ? "#ffaa00"
78
- : session.state === "thinking"
79
- ? CYAN
80
- : GREEN_DARK;
96
+ const dotColor = STATE_COLORS[session.state];
81
97
 
82
98
  if (!session.started) {
83
99
  return (
@@ -192,12 +208,12 @@ function InfocomAdventure() {
192
208
  {msg.content}
193
209
  </div>
194
210
  ))}
195
- {session.userTranscript !== null && (
211
+ {speaking && (
196
212
  <div
197
213
  className="ic-transcript italic"
198
214
  style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
199
215
  >
200
- {session.userTranscript === "" ? "..." : session.userTranscript}
216
+ {userTranscript}
201
217
  </div>
202
218
  )}
203
219
  </AutoScroll>
@@ -1,4 +1,4 @@
1
- import { pushCapped, type SlotStateOf, sessionSlot } from "@alexkroman1/aai";
1
+ import { pushCapped, sessionSlot } from "@alexkroman1/aai";
2
2
 
3
3
  export type GameState = {
4
4
  inventory: string[];
@@ -20,20 +20,37 @@ export const DEFAULT_GAME_STATE: GameState = {
20
20
 
21
21
  /**
22
22
  * How many player commands the game remembers. The history is append-only and
23
- * lives in `ctx.state` for the length of the call, so it needs a cap — and
24
- * only the last few are ever read (`game_state_get` reports five), so the
25
- * older ones are cost without a reader.
23
+ * rides in the slot's stored value for the length of the call, so it needs a
24
+ * cap — and only the last few are ever read (`game_state_get` reports five), so
25
+ * the older ones are cost without a reader.
26
26
  */
27
27
  export const MAX_HISTORY = 50;
28
28
 
29
- // The game lives in `ctx.state`, the agent's per-session mutable state — each
30
- // session is its own playthrough, so concurrent players never see each
31
- // other's game and a fresh session starts a fresh adventure. The clone is
32
- // load-bearing: `DEFAULT_GAME_STATE` is one module-level object shared by
33
- // every session in the process.
34
- export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_GAME_STATE));
29
+ /**
30
+ * How many of those commands a tool reports back to the model.
31
+ *
32
+ * Named because two tools answer with it (`game_state_get` and
33
+ * `game_state_history`), and they live in separate files now — an inline `-5`
34
+ * in each is a number that can disagree with itself.
35
+ */
36
+ export const REPORTED_HISTORY = 5;
35
37
 
36
- export type StateSlot = SlotStateOf<typeof gameSlot>;
38
+ // The game lives in one `sessionSlot`, keyed per session — each session is its
39
+ // own playthrough, so concurrent players never see each other's game and a
40
+ // fresh session starts a fresh adventure. The clone is load-bearing:
41
+ // `DEFAULT_GAME_STATE` is one module-level object shared by every session in
42
+ // the process.
43
+ //
44
+ // Every tool here is declared through the slot, and WHICH HALF is the decision
45
+ // to get right: `gameSlot.tool` hands the body a deep-frozen value and
46
+ // `gameSlot.updateTool` hands it a draft that is stored when the body returns.
47
+ // `game_state_take` and `game_state_flag` were declared with the reading half
48
+ // while pushing to `inventory` and writing to `flags` — a `TypeError` on the
49
+ // first call and every call, once `freezeStorable` deep-froze what a read hands
50
+ // out. It is a compile error now, which is the reason to declare a tool through
51
+ // the slot at all rather than reaching for `gameSlot.get(ctx)` inside a
52
+ // `tool()`.
53
+ export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_GAME_STATE));
37
54
 
38
55
  /** Log a player command, holding {@link MAX_HISTORY}. */
39
56
  export function recordCommand(game: GameState, command: string): void {
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { gameSlot } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Remove an item from the player's inventory.",
6
+ inputSchema: z.object({
7
+ value: z.string().describe("Item name to drop"),
8
+ }),
9
+ execute(args, game) {
10
+ game.inventory = game.inventory.filter((i) => i !== args.value);
11
+ return { inventory: game.inventory };
12
+ },
13
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { gameSlot } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Set a game flag to true, used for tracking puzzle and event state.",
6
+ inputSchema: z.object({
7
+ value: z.string().describe("Flag name to set"),
8
+ }),
9
+ execute(args, game) {
10
+ game.flags[args.value] = true;
11
+ return { flags: game.flags };
12
+ },
13
+ });
@@ -0,0 +1,16 @@
1
+ import { gameSlot, REPORTED_HISTORY } from "../shared.ts";
2
+
3
+ export default gameSlot.tool({
4
+ description:
5
+ "Read the current game state including inventory, current room, score, moves, flags, and recent history.",
6
+ execute(_args, game) {
7
+ return {
8
+ currentRoom: game.currentRoom,
9
+ inventory: game.inventory,
10
+ score: game.score,
11
+ moves: game.moves,
12
+ flags: game.flags,
13
+ recentHistory: game.history.slice(-REPORTED_HISTORY),
14
+ };
15
+ },
16
+ });
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { gameSlot, REPORTED_HISTORY, recordCommand } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Log a player command to the history and increment the move counter.",
6
+ inputSchema: z.object({
7
+ value: z.string().describe("Command text to log"),
8
+ }),
9
+ execute(args, game) {
10
+ recordCommand(game, args.value);
11
+ game.moves++;
12
+ return { moves: game.moves, recentHistory: game.history.slice(-REPORTED_HISTORY) };
13
+ },
14
+ });
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { gameSlot } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Move the player to a new room and increment the move counter.",
6
+ inputSchema: z.object({
7
+ value: z.string().describe("Room name to move to"),
8
+ }),
9
+ execute(args, game) {
10
+ game.currentRoom = args.value;
11
+ game.moves++;
12
+ return { currentRoom: game.currentRoom, moves: game.moves };
13
+ },
14
+ });
@@ -0,0 +1,13 @@
1
+ import { gameSlot } from "../shared.ts";
2
+
3
+ export default gameSlot.tool({
4
+ description:
5
+ "Reset the game to the beginning: empty inventory, zero score and moves, all flags cleared. Use when the player asks to restart, quit, or start a new game.",
6
+ // The one tool here that does NOT want the live value: it replaces it. The
7
+ // slot is still what defines the tool, so the state type comes from the same
8
+ // place as every sibling's rather than from an annotation on `ctx`.
9
+ execute(_args, _game, ctx) {
10
+ const fresh = gameSlot.reset(ctx);
11
+ return { restarted: true, currentRoom: fresh.currentRoom };
12
+ },
13
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { gameSlot } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Add points to the player's score.",
6
+ inputSchema: z.object({
7
+ value: z.number().describe("Points to add"),
8
+ }),
9
+ execute(args, game) {
10
+ game.score += args.value;
11
+ return { score: game.score };
12
+ },
13
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { gameSlot } from "../shared.ts";
3
+
4
+ export default gameSlot.updateTool({
5
+ description: "Add an item to the player's inventory.",
6
+ inputSchema: z.object({
7
+ value: z.string().describe("Item name to take"),
8
+ }),
9
+ execute(args, game) {
10
+ if (!game.inventory.includes(args.value)) game.inventory.push(args.value);
11
+ return { inventory: game.inventory };
12
+ },
13
+ });