@alexkroman1/aai-cli 5.14.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +8 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +17 -19
  53. package/dist/templates/dispatch-center/agent.ts +2 -29
  54. package/dist/templates/dispatch-center/client.tsx +33 -16
  55. package/dist/templates/dispatch-center/shared.ts +53 -19
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +28 -12
  82. package/dist/templates/infocom-adventure/shared.ts +28 -11
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +52 -47
  102. package/dist/templates/pizza-ordering/agent.ts +9 -158
  103. package/dist/templates/pizza-ordering/shared.ts +35 -12
  104. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  105. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  106. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  108. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  109. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  110. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  111. package/dist/templates/plan-and-execute/agent.ts +23 -0
  112. package/dist/templates/plan-and-execute/client.tsx +115 -0
  113. package/dist/templates/plan-and-execute/graph.ts +178 -0
  114. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  115. package/dist/templates/plan-and-execute/shared.ts +161 -0
  116. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  117. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  118. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  119. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  120. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  121. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  122. package/dist/templates/recap-workflow/agent.ts +95 -0
  123. package/dist/templates/recap-workflow/shared.ts +43 -0
  124. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  125. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  126. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  127. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  128. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  129. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  130. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  131. package/dist/templates/redline/agent.test.ts +222 -0
  132. package/dist/templates/redline/agent.ts +89 -0
  133. package/dist/templates/redline/client.tsx +184 -0
  134. package/dist/templates/redline/workflows/prompts.ts +68 -0
  135. package/dist/templates/redline/workflows/redline.ts +258 -0
  136. package/dist/templates/research-workflow/agent.test.ts +525 -0
  137. package/dist/templates/research-workflow/agent.ts +94 -0
  138. package/dist/templates/research-workflow/shared.ts +35 -0
  139. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  140. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  141. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  142. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  143. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  144. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  145. package/dist/templates/retail/agent.test.ts +236 -172
  146. package/dist/templates/retail/agent.ts +5 -42
  147. package/dist/templates/retail/client.tsx +33 -11
  148. package/dist/templates/retail/registry.test.ts +31 -16
  149. package/dist/templates/retail/resolve.ts +28 -64
  150. package/dist/templates/retail/shared.test.ts +4 -3
  151. package/dist/templates/retail/shared.ts +24 -7
  152. package/dist/templates/retail/store.test.ts +12 -10
  153. package/dist/templates/retail/store.ts +69 -30
  154. package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
  155. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
  156. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  157. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  158. package/dist/templates/retail/tools/get_item_details.ts +5 -7
  159. package/dist/templates/retail/tools/get_order_details.ts +5 -7
  160. package/dist/templates/retail/tools/get_product_details.ts +4 -7
  161. package/dist/templates/retail/tools/get_user_details.ts +4 -7
  162. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  163. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
  164. package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
  165. package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
  166. package/dist/templates/retail/tools/modify_user_address.ts +4 -9
  167. package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
  168. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  169. package/dist/templates/solo-rpg/agent.test.ts +152 -29
  170. package/dist/templates/solo-rpg/agent.ts +3 -21
  171. package/dist/templates/solo-rpg/client.tsx +19 -6
  172. package/dist/templates/solo-rpg/shared.ts +19 -11
  173. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
  174. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
  175. package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
  176. package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
  177. package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
  178. package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
  179. package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
  180. package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
  181. package/dist/templates/support-line/agent.test.ts +343 -0
  182. package/dist/templates/support-line/agent.ts +27 -0
  183. package/dist/templates/support-line/client.tsx +122 -0
  184. package/dist/templates/support-line/graph.ts +224 -0
  185. package/dist/templates/support-line/knowledge.json +65 -0
  186. package/dist/templates/support-line/prompts.ts +95 -0
  187. package/dist/templates/support-line/shared.ts +240 -0
  188. package/dist/templates/support-line/system-prompt.md +17 -0
  189. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  190. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  191. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  193. package/dist/templates/transcription-workflow/agent.ts +92 -0
  194. package/dist/templates/transcription-workflow/client.tsx +234 -0
  195. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  196. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  197. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  198. package/dist/templates/travel-concierge/agent.ts +29 -0
  199. package/dist/templates/travel-concierge/client.tsx +161 -0
  200. package/dist/templates/travel-concierge/routing.ts +138 -0
  201. package/dist/templates/travel-concierge/shared.ts +547 -0
  202. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  203. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  204. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  205. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  206. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  207. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  208. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  209. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  210. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  211. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  212. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  213. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  214. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  215. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  216. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  217. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  218. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  219. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  220. package/dist/templates/web-researcher/agent.ts +0 -2
  221. package/dist/templates/web-researcher/system-prompt.md +10 -0
  222. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  223. package/dist/typecheck.mjs +17 -15
  224. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  225. package/dist/worker-bundler.d.ts +14 -0
  226. package/dist/worker-bundler.mjs +1 -108
  227. package/dist/workflow-B-O97AqI.mjs +125 -0
  228. package/dist/workflow-bundler.d.ts +133 -0
  229. package/dist/workflow.d.ts +67 -0
  230. package/package.json +13 -9
  231. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -1,36 +1,37 @@
1
- import type { ToolContext, ToolDef } from "@alexkroman1/aai";
2
- import { createToolContext } from "@alexkroman1/aai/testing";
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { ToolContext } from "@alexkroman1/aai";
4
+ import { createToolContext, runTool, toolOf, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
5
  import { describe, expect, test } from "vitest";
4
- import agentDef from "./agent.ts";
5
- import {
6
- calculateTotal,
7
- orderSlot,
8
- orderView,
9
- type Pizza,
10
- pizzaPrice,
11
- type StateSlot,
12
- } 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";
13
22
 
14
23
  // ─── Test doubles ────────────────────────────────────────────────────────────
15
24
 
16
- /** Each context is one session the cart is session-scoped by construction,
17
- * and `createToolContext` mints a distinct session id per call. Its default
18
- * `db` rejects every query, which is right here: this template keeps its cart
19
- * in ctx.state and must never touch storage. */
20
- function makeCtx(sessionId?: string) {
21
- const ctx = createToolContext<StateSlot>(sessionId ? { sessionId } : {});
22
- return { ctx, sent: ctx.sent };
23
- }
24
-
25
- function getTool(name: string): ToolDef {
26
- const def = agentDef.tools[name];
27
- if (!def) throw new Error(`tool ${name} not defined on agent`);
28
- return def;
29
- }
30
-
31
- async function run(name: string, args: Record<string, unknown>, ctx: ToolContext) {
32
- return await getTool(name).execute(args, ctx);
33
- }
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();
29
+
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);
34
35
 
35
36
  const margherita: Omit<Pizza, "id"> = {
36
37
  size: "medium",
@@ -71,7 +72,7 @@ describe("pricing (shared.ts)", () => {
71
72
  });
72
73
 
73
74
  test("add_pizza schema defaults quantity to 1 and rejects non-positive quantities", async () => {
74
- const schema = getTool("add_pizza").inputSchema;
75
+ const schema = toolOf(agentDef, "add_pizza").inputSchema;
75
76
  if (!schema) throw new Error("add_pizza has no input schema");
76
77
  // Validate through the Standard Schema contract — the vendor-neutral
77
78
  // interface every inputSchema carries.
@@ -90,7 +91,7 @@ describe("pricing (shared.ts)", () => {
90
91
 
91
92
  describe("tool flow (add → update → remove → place_order)", () => {
92
93
  test("full ordering round-trip keeps state, totals, and IDs consistent", async () => {
93
- const { ctx } = makeCtx();
94
+ const ctx = makeCtx();
94
95
 
95
96
  // Empty order guards
96
97
  expect(await run("view_order", {}, ctx)).toEqual({ message: "The order is empty." });
@@ -151,31 +152,35 @@ describe("tool flow (add → update → remove → place_order)", () => {
151
152
  expect(await run("place_order", {}, ctx)).toEqual({ error: "Cannot place an empty order." });
152
153
  });
153
154
 
154
- test("carts are scoped per session — two sessions never share pizzas or names", async () => {
155
- // ctx.state is per-session by construction each session has its own.
156
- const { ctx: sessionA } = makeCtx("session-a");
157
- 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();
158
163
 
159
164
  await run(
160
165
  "add_pizza",
161
166
  { size: "large", crust: "thin", toppings: ["bacon"], quantity: 1 },
162
- sessionA,
167
+ firstCall,
163
168
  );
164
- await run("set_customer_name", { name: "Alice" }, sessionA);
169
+ await run("set_customer_name", { name: "Alice" }, firstCall);
165
170
 
166
- 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." });
167
172
 
168
- await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, sessionB);
169
- 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 {
170
175
  customerName: string;
171
176
  pizzas: number;
172
177
  };
173
- // Session B never sees session A's customer name or pizzas.
178
+ // The second context never sees the first's customer name or pizzas.
174
179
  expect(placedB.customerName).toBe("Guest");
175
180
  expect(placedB.pizzas).toBe(1);
176
181
 
177
- // Session A's cart survives session B's checkout untouched.
178
- 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[] };
179
184
  expect(viewA.pizzas).toHaveLength(1);
180
185
  });
181
186
  });
@@ -188,14 +193,14 @@ describe("tool flow (add → update → remove → place_order)", () => {
188
193
 
189
194
  describe("orderView projection", () => {
190
195
  test("reflects the live cart", async () => {
191
- const { ctx } = makeCtx();
196
+ const ctx = makeCtx();
192
197
  await run(
193
198
  "add_pizza",
194
199
  { size: "large", crust: "stuffed", toppings: ["pepperoni", "extra_cheese"], quantity: 2 },
195
200
  ctx,
196
201
  );
197
202
 
198
- const view = orderView(orderSlot.read(ctx.state));
203
+ const view = orderView(orderSlot.get(ctx));
199
204
  expect(view.orderPlaced).toBe(false);
200
205
  const item = view.pizzas[0];
201
206
  if (!item) throw new Error("no pizza in the projection");
@@ -213,11 +218,11 @@ describe("orderView projection", () => {
213
218
  test("survives checkout, which clears the cart but keeps the confirmation", async () => {
214
219
  // The reason `placed` lives in state at all: the cart is emptied on
215
220
  // checkout, and the UI still has to show the order that was just placed.
216
- const { ctx } = makeCtx();
221
+ const ctx = makeCtx();
217
222
  await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, ctx);
218
223
  await run("place_order", {}, ctx);
219
224
 
220
- const view = orderView(orderSlot.read(ctx.state));
225
+ const view = orderView(orderSlot.get(ctx));
221
226
  expect(view.orderPlaced).toBe(true);
222
227
  expect(view.pizzas).toEqual([]);
223
228
  expect(view.estimatedMinutes).toBe(20);
@@ -1,26 +1,17 @@
1
- import { agent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import {
4
- CRUSTS,
5
- calculateTotal,
6
- formatPrice,
7
- menuText,
8
- orderSlot,
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.
@@ -30,144 +21,4 @@ export default agent({
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 = orderSlot.get(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 = orderSlot.get(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 = orderSlot.get(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
- orderSlot.get(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 = orderSlot.get(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 = orderSlot.get(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,4 +1,4 @@
1
- import { type SlotStateOf, sessionSlot, 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[];
@@ -93,19 +93,40 @@ export function emptyOrder(): OrderState {
93
93
  return { pizzas: [], nextId: 1, customerName: null };
94
94
  }
95
95
 
96
- /** The session's cart, as one typed slot inside `ctx.state`. */
96
+ /** The session's cart, as one typed slot. */
97
97
  export const orderSlot = sessionSlot("order", emptyOrder);
98
98
 
99
- export type StateSlot = SlotStateOf<typeof orderSlot>;
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>;
100
110
 
101
111
  /**
102
112
  * Clear the cart after checkout so a follow-up order starts fresh. The
103
113
  * `placed` confirmation is passed through, because the UI is still showing
104
114
  * it — clearing the pizzas is what "reset" means here, not forgetting the
105
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.
106
122
  */
107
- export function resetOrder(ctx: ToolContext, placed?: OrderState["placed"]): void {
108
- orderSlot.set(ctx, { ...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;
109
130
  }
110
131
 
111
132
  /**
@@ -114,7 +135,9 @@ export function resetOrder(ctx: ToolContext, placed?: OrderState["placed"]): voi
114
135
  * leaves the server.
115
136
  */
116
137
  export interface OrderView {
117
- 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[];
118
141
  total: string;
119
142
  orderPlaced: boolean;
120
143
  orderNumber?: number;
@@ -123,7 +146,7 @@ export interface OrderView {
123
146
 
124
147
  /** Takes the cart itself, not the slot — `orderSlot.projection` supplies a real
125
148
  * one even before the first tool call, so there is nothing to optional-chain. */
126
- export function orderView(order: OrderState): OrderView {
149
+ export function orderView(order: FrozenOrderState): OrderView {
127
150
  const placed = order.placed;
128
151
  return {
129
152
  pizzas: order.pizzas,
@@ -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
+ });
@@ -0,0 +1,21 @@
1
+ import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
2
+
3
+ export default orderSlot.tool({
4
+ description: "View the current order summary with all pizzas and total price.",
5
+ execute(_args, order) {
6
+ const pizzas = order.pizzas;
7
+ if (pizzas.length === 0) return { message: "The order is empty." };
8
+
9
+ return {
10
+ pizzas: pizzas.map((p) => ({
11
+ id: p.id,
12
+ description: `${p.quantity}x ${p.size} ${p.crust} crust with ${p.toppings.length > 0 ? p.toppings.join(", ") : "cheese only"}`,
13
+ size: p.size,
14
+ crust: p.crust,
15
+ toppings: p.toppings,
16
+ quantity: p.quantity,
17
+ })),
18
+ orderTotal: formatPrice(calculateTotal(pizzas)),
19
+ };
20
+ },
21
+ });