@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,45 +1,37 @@
1
- import type { Db, ToolContext, ToolDef } from "@alexkroman1/aai";
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { ToolContext } from "@alexkroman1/aai";
4
+ import { createToolContext, runTool, toolOf, withDiscoveredTools } from "@alexkroman1/aai/testing";
2
5
  import { describe, expect, test } from "vitest";
3
- import agentDef from "./agent.ts";
4
- import { calculateTotal, orderView, type Pizza, pizzaPrice, type StateSlot } from "./shared.ts";
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 { calculateTotal, orderSlot, orderView, type Pizza, pizzaPrice } from "./shared.ts";
5
22
 
6
23
  // ─── Test doubles ────────────────────────────────────────────────────────────
7
24
 
8
- /** This template keeps its cart in ctx.state the db must never be touched. */
9
- const noDb: Db = {
10
- query: () => Promise.reject(new Error("db not used by this template")),
11
- };
25
+ /** Each context owns its OWN slot store, so two contexts are two carts. Its
26
+ * default `db` rejects every query, which is right here: this template keeps
27
+ * its cart in a session slot and must never touch storage. */
28
+ const makeCtx = (): ToolContext => createToolContext();
12
29
 
13
- /** ToolContext stub: per-session ctx.state, and captured ctx.send events.
14
- * Each stub is one session the cart is session-scoped by construction. */
15
- function makeCtx(sessionId = "session-a") {
16
- const sent: Array<{ event: string; data: unknown }> = [];
17
- const ctx: ToolContext = {
18
- env: {},
19
- state: {},
20
- db: noDb,
21
- generate: () => Promise.reject(new Error("generate not available in tests")),
22
- messages: [],
23
- sessionId,
24
- // Never aborts — a test has no turn to cancel. `ctx.signal` is always
25
- // present at runtime, so a stub supplies one rather than omitting it.
26
- signal: new AbortController().signal,
27
- send: (event, data) => {
28
- sent.push({ event, data });
29
- },
30
- };
31
- return { ctx, sent };
32
- }
33
-
34
- function getTool(name: string): ToolDef {
35
- const def = agentDef.tools[name];
36
- if (!def) throw new Error(`tool ${name} not defined on agent`);
37
- return def;
38
- }
39
-
40
- async function run(name: string, args: Record<string, unknown>, ctx: ToolContext) {
41
- return await getTool(name).execute(args, ctx);
42
- }
30
+ /** A tool by the name the model calls it by, bound to this agent. The lookup
31
+ * and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
32
+ * what is local is only which agent they run against. */
33
+ const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
34
+ runTool(agentDef, name, args, ctx);
43
35
 
44
36
  const margherita: Omit<Pizza, "id"> = {
45
37
  size: "medium",
@@ -80,7 +72,7 @@ describe("pricing (shared.ts)", () => {
80
72
  });
81
73
 
82
74
  test("add_pizza schema defaults quantity to 1 and rejects non-positive quantities", async () => {
83
- const schema = getTool("add_pizza").inputSchema;
75
+ const schema = toolOf(agentDef, "add_pizza").inputSchema;
84
76
  if (!schema) throw new Error("add_pizza has no input schema");
85
77
  // Validate through the Standard Schema contract — the vendor-neutral
86
78
  // interface every inputSchema carries.
@@ -99,7 +91,7 @@ describe("pricing (shared.ts)", () => {
99
91
 
100
92
  describe("tool flow (add → update → remove → place_order)", () => {
101
93
  test("full ordering round-trip keeps state, totals, and IDs consistent", async () => {
102
- const { ctx } = makeCtx();
94
+ const ctx = makeCtx();
103
95
 
104
96
  // Empty order guards
105
97
  expect(await run("view_order", {}, ctx)).toEqual({ message: "The order is empty." });
@@ -160,31 +152,35 @@ describe("tool flow (add → update → remove → place_order)", () => {
160
152
  expect(await run("place_order", {}, ctx)).toEqual({ error: "Cannot place an empty order." });
161
153
  });
162
154
 
163
- test("carts are scoped per session — two sessions never share pizzas or names", async () => {
164
- // ctx.state is per-session by construction each session has its own.
165
- const { ctx: sessionA } = makeCtx("session-a");
166
- const { ctx: sessionB } = makeCtx("session-b");
155
+ test("two independent contexts never share pizzas or names", async () => {
156
+ // What this really checks, and it is worth checking: the cart lives in the
157
+ // SLOT and not in a module-level variable. `createToolContext()` hands each
158
+ // call its own detached slot store, so passing two distinct session ids
159
+ // would prove nothing extra — the isolation is per store, and the store is
160
+ // per context. A template that cached its order in a module would fail here.
161
+ const firstCall = makeCtx();
162
+ const secondCall = makeCtx();
167
163
 
168
164
  await run(
169
165
  "add_pizza",
170
166
  { size: "large", crust: "thin", toppings: ["bacon"], quantity: 1 },
171
- sessionA,
167
+ firstCall,
172
168
  );
173
- await run("set_customer_name", { name: "Alice" }, sessionA);
169
+ await run("set_customer_name", { name: "Alice" }, firstCall);
174
170
 
175
- expect(await run("view_order", {}, sessionB)).toEqual({ message: "The order is empty." });
171
+ expect(await run("view_order", {}, secondCall)).toEqual({ message: "The order is empty." });
176
172
 
177
- await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, sessionB);
178
- const placedB = (await run("place_order", {}, sessionB)) as {
173
+ await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, secondCall);
174
+ const placedB = (await run("place_order", {}, secondCall)) as {
179
175
  customerName: string;
180
176
  pizzas: number;
181
177
  };
182
- // Session B never sees session A's customer name or pizzas.
178
+ // The second context never sees the first's customer name or pizzas.
183
179
  expect(placedB.customerName).toBe("Guest");
184
180
  expect(placedB.pizzas).toBe(1);
185
181
 
186
- // Session A's cart survives session B's checkout untouched.
187
- const viewA = (await run("view_order", {}, sessionA)) as { pizzas: unknown[] };
182
+ // And the first context's cart survives the second's checkout untouched.
183
+ const viewA = (await run("view_order", {}, firstCall)) as { pizzas: unknown[] };
188
184
  expect(viewA.pizzas).toHaveLength(1);
189
185
  });
190
186
  });
@@ -197,14 +193,14 @@ describe("tool flow (add → update → remove → place_order)", () => {
197
193
 
198
194
  describe("orderView projection", () => {
199
195
  test("reflects the live cart", async () => {
200
- const { ctx } = makeCtx();
196
+ const ctx = makeCtx();
201
197
  await run(
202
198
  "add_pizza",
203
199
  { size: "large", crust: "stuffed", toppings: ["pepperoni", "extra_cheese"], quantity: 2 },
204
200
  ctx,
205
201
  );
206
202
 
207
- const view = orderView(ctx.state as StateSlot);
203
+ const view = orderView(orderSlot.get(ctx));
208
204
  expect(view.orderPlaced).toBe(false);
209
205
  const item = view.pizzas[0];
210
206
  if (!item) throw new Error("no pizza in the projection");
@@ -222,19 +218,24 @@ describe("orderView projection", () => {
222
218
  test("survives checkout, which clears the cart but keeps the confirmation", async () => {
223
219
  // The reason `placed` lives in state at all: the cart is emptied on
224
220
  // checkout, and the UI still has to show the order that was just placed.
225
- const { ctx } = makeCtx();
221
+ const ctx = makeCtx();
226
222
  await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, ctx);
227
223
  await run("place_order", {}, ctx);
228
224
 
229
- const view = orderView(ctx.state as StateSlot);
225
+ const view = orderView(orderSlot.get(ctx));
230
226
  expect(view.orderPlaced).toBe(true);
231
227
  expect(view.pizzas).toEqual([]);
232
228
  expect(view.estimatedMinutes).toBe(20);
233
229
  expect(view.total).toMatch(/^\$\d+\.\d{2}$/);
234
230
  });
235
231
 
236
- test("an untouched session projects an empty cart, not undefined", async () => {
237
- // The client renders before any tool has run; `state.order` is absent.
238
- expect(orderView({})).toMatchObject({ pizzas: [], total: "$0.00", orderPlaced: false });
232
+ test("an untouched session projects an empty cart, not undefined", () => {
233
+ // The client renders before any tool has run, so `state.order` is absent
234
+ // this is exactly the value `client.tsx` hoists as its fallback.
235
+ expect(orderSlot.projection(orderView)(undefined)).toMatchObject({
236
+ pizzas: [],
237
+ total: "$0.00",
238
+ orderPlaced: false,
239
+ });
239
240
  });
240
241
  });
@@ -1,173 +1,24 @@
1
- import { agent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import {
4
- CRUSTS,
5
- calculateTotal,
6
- formatPrice,
7
- getOrder,
8
- menuText,
9
- orderView,
10
- type Pizza,
11
- resetOrder,
12
- SIZES,
13
- } from "./shared.ts";
1
+ import { agent } from "@alexkroman1/aai";
2
+ import { menuText, orderSlot, orderView } from "./shared.ts";
14
3
  import systemPrompt from "./system-prompt.md?raw";
15
4
 
16
- const sizes = z.enum(SIZES);
17
- const crusts = z.enum(CRUSTS);
18
-
19
- // The in-progress order lives in `ctx.state` (see shared.ts) per-session by
20
- // construction, so concurrent customers each get their own cart.
5
+ // The in-progress order lives in one `sessionSlot` (see shared.ts)
6
+ // per-session by construction, so concurrent customers each get their own cart.
7
+ // The six tools that read and write it are the six files in `tools/`: a file
8
+ // there IS a tool, named by its own filename, so nothing registers them here.
21
9
 
22
10
  export default agent({
23
11
  name: "Pizza Palace",
12
+ // `orderSlot.state` IS the `() => ({ [slot.key]: slot.create() })` factory,
13
+ // so the session's cart exists before the first tool call — which is what a
14
+ // resumed connection needs to have something to project.
24
15
  // The cart, pushed to the client after every tool call. Replaces a
25
16
  // `ctx.send("order", ...)` in each of the five order tools, and the
26
17
  // event-diffing the client had to do to rebuild the cart from them.
27
- syncState: orderView,
18
+ syncState: orderSlot.projection(orderView),
28
19
  // The menu section is generated from MENU so the prompt can never quote a
29
20
  // price the pricing code doesn't charge.
30
21
  systemPrompt: `${systemPrompt}\n${menuText()}`,
31
22
  greeting:
32
23
  "Welcome to Pizza Palace. I can help you build your perfect pizza. What would you like to order?",
33
-
34
- tools: {
35
- add_pizza: tool({
36
- description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
37
- inputSchema: z.object({
38
- size: sizes,
39
- crust: crusts,
40
- toppings: z
41
- .array(z.string())
42
- .describe("List of topping names, e.g. ['pepperoni', 'mushrooms']"),
43
- quantity: z.number().int().min(1).default(1),
44
- }),
45
- async execute(args, ctx) {
46
- const order = getOrder(ctx);
47
-
48
- const pizza: Pizza = {
49
- id: order.nextId,
50
- size: args.size,
51
- crust: args.crust,
52
- toppings: args.toppings,
53
- quantity: args.quantity,
54
- };
55
- order.pizzas.push(pizza);
56
- order.nextId++;
57
-
58
- return {
59
- added: pizza,
60
- orderTotal: formatPrice(calculateTotal(order.pizzas)),
61
- itemCount: order.pizzas.length,
62
- };
63
- },
64
- }),
65
-
66
- place_order: tool({
67
- description:
68
- "Place the final order. Use when the customer confirms they are done and ready to order.",
69
- async execute(_args, ctx) {
70
- const order = getOrder(ctx);
71
- const pizzas = order.pizzas;
72
- if (pizzas.length === 0) return { error: "Cannot place an empty order." };
73
-
74
- const customerName = order.customerName ?? "Guest";
75
- const total = formatPrice(calculateTotal(pizzas));
76
- const orderNumber = Math.floor(1000 + Math.random() * 9000);
77
-
78
- const estimatedMinutes = 15 + pizzas.length * 5;
79
- // The order is submitted — clear the cart so a follow-up order starts
80
- // fresh, but keep the confirmation in state so the UI can show it.
81
- resetOrder(ctx, { orderNumber, total, estimatedMinutes });
82
-
83
- return {
84
- orderNumber,
85
- customerName,
86
- pizzas: pizzas.length,
87
- total,
88
- estimatedMinutes,
89
- };
90
- },
91
- }),
92
-
93
- remove_pizza: tool({
94
- description: "Remove a pizza from the order by its ID.",
95
- inputSchema: z.object({
96
- pizza_id: z.number().describe("The pizza ID to remove"),
97
- }),
98
- async execute(args, ctx) {
99
- const order = getOrder(ctx);
100
- const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
101
- if (idx === -1) return { error: "Pizza not found in the order." };
102
-
103
- const [removed] = order.pizzas.splice(idx, 1);
104
-
105
- return {
106
- removed,
107
- orderTotal: formatPrice(calculateTotal(order.pizzas)),
108
- itemCount: order.pizzas.length,
109
- };
110
- },
111
- }),
112
-
113
- set_customer_name: tool({
114
- description: "Set the customer name for the order.",
115
- inputSchema: z.object({
116
- name: z.string(),
117
- }),
118
- async execute(args, ctx) {
119
- getOrder(ctx).customerName = args.name;
120
- return { name: args.name };
121
- },
122
- }),
123
-
124
- update_pizza: tool({
125
- description: "Update an existing pizza in the order. Only provided fields are changed.",
126
- inputSchema: z.object({
127
- pizza_id: z.number(),
128
- size: sizes.optional(),
129
- crust: crusts.optional(),
130
- toppings: z.array(z.string()).optional(),
131
- quantity: z.number().int().min(1).optional(),
132
- }),
133
- async execute(args, ctx) {
134
- const order = getOrder(ctx);
135
- const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
136
- if (idx === -1) return { error: "Pizza not found in the order." };
137
-
138
- const pizza: Pizza = { ...order.pizzas[idx]! };
139
- if (args.size !== undefined) pizza.size = args.size;
140
- if (args.crust !== undefined) pizza.crust = args.crust;
141
- if (args.toppings !== undefined) pizza.toppings = args.toppings;
142
- if (args.quantity !== undefined) pizza.quantity = args.quantity;
143
-
144
- order.pizzas[idx] = pizza;
145
-
146
- return {
147
- updated: pizza,
148
- orderTotal: formatPrice(calculateTotal(order.pizzas)),
149
- };
150
- },
151
- }),
152
-
153
- view_order: tool({
154
- description: "View the current order summary with all pizzas and total price.",
155
- async execute(_args, ctx) {
156
- const pizzas = getOrder(ctx).pizzas;
157
- if (pizzas.length === 0) return { message: "The order is empty." };
158
-
159
- return {
160
- pizzas: pizzas.map((p) => ({
161
- id: p.id,
162
- description: `${p.quantity}x ${p.size} ${p.crust} crust with ${p.toppings.length > 0 ? p.toppings.join(", ") : "cheese only"}`,
163
- size: p.size,
164
- crust: p.crust,
165
- toppings: p.toppings,
166
- quantity: p.quantity,
167
- })),
168
- orderTotal: formatPrice(calculateTotal(pizzas)),
169
- };
170
- },
171
- }),
172
- },
173
24
  });
@@ -1,11 +1,11 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
2
  import { client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
3
3
  import type { OrderView } from "./shared.ts";
4
- import { formatPrice, orderView, pizzaPrice } from "./shared.ts";
4
+ import { formatPrice, orderSlot, orderView, pizzaPrice } from "./shared.ts";
5
5
 
6
6
  // Derived from the projection so a new OrderView field can't silently miss
7
- // the pre-first-tool-call render.
8
- const EMPTY_ORDER = orderView({});
7
+ // the pre-first-tool-call render — `projection` runs it over an empty cart.
8
+ const EMPTY_ORDER: OrderView = orderSlot.projection(orderView)(undefined);
9
9
 
10
10
  function PizzaIcon({ size }: { size: string }) {
11
11
  const dim = size === "small" ? 36 : size === "large" ? 52 : 44;
@@ -30,7 +30,7 @@ function OrderSidebar() {
30
30
  // tool call. This replaced ~45 lines that rebuilt the cart by diffing
31
31
  // added/removed/updated events — where one missed event desynced the view
32
32
  // for the rest of the session.
33
- const order = useAgentState<OrderView>() ?? EMPTY_ORDER;
33
+ const order = useAgentState<OrderView>(EMPTY_ORDER);
34
34
 
35
35
  if (order.orderPlaced) {
36
36
  return (
@@ -1,4 +1,4 @@
1
- import type { ToolContext } from "@alexkroman1/aai";
1
+ import { type DeepReadonly, sessionSlot } from "@alexkroman1/aai";
2
2
 
3
3
  export const SIZES = ["small", "medium", "large"] as const;
4
4
  export const CRUSTS = ["thin", "regular", "thick", "stuffed"] as const;
@@ -33,7 +33,7 @@ export const MENU = {
33
33
  },
34
34
  } as const;
35
35
 
36
- export function calculateTotal(pizzas: Pizza[]): number {
36
+ export function calculateTotal(pizzas: readonly ReadonlyPizza[]): number {
37
37
  return pizzas.reduce((total, pizza) => total + pizzaPrice(pizza), 0);
38
38
  }
39
39
 
@@ -62,7 +62,7 @@ export function menuText(): string {
62
62
  ].join("\n");
63
63
  }
64
64
 
65
- export function pizzaPrice(p: Pizza): number {
65
+ export function pizzaPrice(p: ReadonlyPizza): number {
66
66
  const base = MENU.sizes[p.size];
67
67
  const crust = MENU.crusts[p.crust];
68
68
  const toppings = p.toppings.reduce(
@@ -73,9 +73,9 @@ export function pizzaPrice(p: Pizza): number {
73
73
  }
74
74
 
75
75
  // ── Order state ──────────────────────────────────────────────────────────────
76
- // The in-progress order is session-scoped scratch, so it lives in `ctx.state`
77
- // (the agent's per-session mutable state) — concurrent customers each get
78
- // their own cart, and an abandoned cart vanishes with its session.
76
+ // The in-progress order is session-scoped scratch, so it lives in one
77
+ // `sessionSlot` keyed per session — concurrent customers each get their own
78
+ // cart.
79
79
 
80
80
  export interface OrderState {
81
81
  pizzas: Pizza[];
@@ -89,51 +89,68 @@ export interface OrderState {
89
89
  placed?: { orderNumber: number; total: string; estimatedMinutes: number };
90
90
  }
91
91
 
92
- export type StateSlot = { order?: OrderState };
93
-
94
- function emptyOrder(): OrderState {
92
+ export function emptyOrder(): OrderState {
95
93
  return { pizzas: [], nextId: 1, customerName: null };
96
94
  }
97
95
 
98
- /** The session's live cart. Mutations to the returned object stick — it is
99
- * the object stored in `ctx.state`. */
100
- export function getOrder(ctx: ToolContext): OrderState {
101
- const slot = ctx.state as StateSlot;
102
- slot.order ??= emptyOrder();
103
- return slot.order;
104
- }
96
+ /** The session's cart, as one typed slot. */
97
+ export const orderSlot = sessionSlot("order", emptyOrder);
98
+
99
+ /**
100
+ * A pizza as a READ hands it out, and the cart likewise: deep-frozen, and typed
101
+ * to say so.
102
+ *
103
+ * `pizzaPrice` and `calculateTotal` take the readonly shape because they are
104
+ * pure and are called from both halves of the slot — a mutable `Pizza` still
105
+ * satisfies it, so an `updateTool` draft passes unchanged, while a helper that
106
+ * WOULD have mutated stops compiling instead of throwing on its first call.
107
+ */
108
+ export type ReadonlyPizza = DeepReadonly<Pizza>;
109
+ export type FrozenOrderState = DeepReadonly<OrderState>;
105
110
 
106
111
  /**
107
112
  * Clear the cart after checkout so a follow-up order starts fresh. The
108
113
  * `placed` confirmation is passed through, because the UI is still showing
109
114
  * it — clearing the pizzas is what "reset" means here, not forgetting the
110
115
  * order that was just submitted.
116
+ *
117
+ * It takes the DRAFT, not the context, and that is the shape to copy. It used to
118
+ * call `orderSlot.set(ctx, …)` — which is a second write to the same slot from
119
+ * inside a mutating tool body, so the draft stored on the way out overwrote it
120
+ * and the cart was never cleared. `slot.set` refuses that outright now; a helper
121
+ * called from inside a mutation mutates what it was handed.
111
122
  */
112
- export function resetOrder(ctx: ToolContext, placed?: OrderState["placed"]): void {
113
- const slot = ctx.state as StateSlot;
114
- slot.order = { ...emptyOrder(), ...(placed ? { placed } : {}) };
123
+ export function resetOrder(order: OrderState, placed?: OrderState["placed"]): void {
124
+ const fresh = emptyOrder();
125
+ order.pizzas = fresh.pizzas;
126
+ order.nextId = fresh.nextId;
127
+ order.customerName = fresh.customerName;
128
+ if (placed) order.placed = placed;
129
+ else delete order.placed;
115
130
  }
116
131
 
117
132
  /**
118
- * What the browser sees. A projection rather than the raw slot: the client
133
+ * What the browser sees. A projection rather than the raw state: the client
119
134
  * needs the cart and its total, and `syncState` is where you decide what
120
135
  * leaves the server.
121
136
  */
122
137
  export interface OrderView {
123
- pizzas: Pizza[];
138
+ /** Readonly, because the projection hands the client the slot's own list
139
+ * rather than a copy — see {@link ReadonlyPizza}. */
140
+ pizzas: readonly ReadonlyPizza[];
124
141
  total: string;
125
142
  orderPlaced: boolean;
126
143
  orderNumber?: number;
127
144
  estimatedMinutes?: number;
128
145
  }
129
146
 
130
- export function orderView(state: StateSlot): OrderView {
131
- const order = state.order;
132
- const pizzas = order?.pizzas ?? [];
133
- const placed = order?.placed;
147
+ /** Takes the cart itself, not the slot — `orderSlot.projection` supplies a real
148
+ * one even before the first tool call, so there is nothing to optional-chain. */
149
+ export function orderView(order: FrozenOrderState): OrderView {
150
+ const placed = order.placed;
134
151
  return {
135
- pizzas,
136
- total: placed?.total ?? formatPrice(calculateTotal(pizzas)),
152
+ pizzas: order.pizzas,
153
+ total: placed?.total ?? formatPrice(calculateTotal(order.pizzas)),
137
154
  orderPlaced: Boolean(placed),
138
155
  ...(placed
139
156
  ? { orderNumber: placed.orderNumber, estimatedMinutes: placed.estimatedMinutes }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
3
+
4
+ export default orderSlot.updateTool({
5
+ description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
6
+ inputSchema: z.object({
7
+ size: z.enum(SIZES),
8
+ crust: z.enum(CRUSTS),
9
+ toppings: z
10
+ .array(z.string())
11
+ .describe("List of topping names, e.g. ['pepperoni', 'mushrooms']"),
12
+ quantity: z.number().int().min(1).default(1),
13
+ }),
14
+ execute(args, order) {
15
+ const pizza: Pizza = {
16
+ id: order.nextId,
17
+ size: args.size,
18
+ crust: args.crust,
19
+ toppings: args.toppings,
20
+ quantity: args.quantity,
21
+ };
22
+ order.pizzas.push(pizza);
23
+ order.nextId++;
24
+
25
+ return {
26
+ added: pizza,
27
+ orderTotal: formatPrice(calculateTotal(order.pizzas)),
28
+ itemCount: order.pizzas.length,
29
+ };
30
+ },
31
+ });
@@ -0,0 +1,22 @@
1
+ import { toolFailure } from "@alexkroman1/aai";
2
+ import { calculateTotal, formatPrice, orderSlot, resetOrder } from "../shared.ts";
3
+
4
+ export default orderSlot.updateTool({
5
+ description:
6
+ "Place the final order. Use when the customer confirms they are done and ready to order.",
7
+ execute(_args, order) {
8
+ const pizzas = order.pizzas;
9
+ if (pizzas.length === 0) return toolFailure("Cannot place an empty order.");
10
+
11
+ const customerName = order.customerName ?? "Guest";
12
+ const total = formatPrice(calculateTotal(pizzas));
13
+ const orderNumber = Math.floor(1000 + Math.random() * 9000);
14
+
15
+ const estimatedMinutes = 15 + pizzas.length * 5;
16
+ // The order is submitted — clear the cart so a follow-up order starts
17
+ // fresh, but keep the confirmation in state so the UI can show it.
18
+ resetOrder(order, { orderNumber, total, estimatedMinutes });
19
+
20
+ return { orderNumber, customerName, pizzas: pizzas.length, total, estimatedMinutes };
21
+ },
22
+ });
@@ -0,0 +1,22 @@
1
+ import { toolFailure } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
4
+
5
+ export default orderSlot.updateTool({
6
+ description: "Remove a pizza from the order by its ID.",
7
+ inputSchema: z.object({
8
+ pizza_id: z.number().describe("The pizza ID to remove"),
9
+ }),
10
+ execute(args, order) {
11
+ const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
12
+ if (idx === -1) return toolFailure("Pizza not found in the order.");
13
+
14
+ const [removed] = order.pizzas.splice(idx, 1);
15
+
16
+ return {
17
+ removed,
18
+ orderTotal: formatPrice(calculateTotal(order.pizzas)),
19
+ itemCount: order.pizzas.length,
20
+ };
21
+ },
22
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { orderSlot } from "../shared.ts";
3
+
4
+ export default orderSlot.updateTool({
5
+ description: "Set the customer name for the order.",
6
+ inputSchema: z.object({
7
+ name: z.string(),
8
+ }),
9
+ execute(args, order) {
10
+ order.customerName = args.name;
11
+ return { name: args.name };
12
+ },
13
+ });
@@ -0,0 +1,28 @@
1
+ import { toolFailure } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
4
+
5
+ export default orderSlot.updateTool({
6
+ description: "Update an existing pizza in the order. Only provided fields are changed.",
7
+ inputSchema: z.object({
8
+ pizza_id: z.number(),
9
+ size: z.enum(SIZES).optional(),
10
+ crust: z.enum(CRUSTS).optional(),
11
+ toppings: z.array(z.string()).optional(),
12
+ quantity: z.number().int().min(1).optional(),
13
+ }),
14
+ execute(args, order) {
15
+ const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
16
+ if (idx === -1) return toolFailure("Pizza not found in the order.");
17
+
18
+ const pizza: Pizza = { ...order.pizzas[idx]! };
19
+ if (args.size !== undefined) pizza.size = args.size;
20
+ if (args.crust !== undefined) pizza.crust = args.crust;
21
+ if (args.toppings !== undefined) pizza.toppings = args.toppings;
22
+ if (args.quantity !== undefined) pizza.quantity = args.quantity;
23
+
24
+ order.pizzas[idx] = pizza;
25
+
26
+ return { updated: pizza, orderTotal: formatPrice(calculateTotal(order.pizzas)) };
27
+ },
28
+ });