@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,11 +1,12 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import seedJson from "./seed.json";
3
- import type { RetailState, Store } from "./shared.ts";
4
- import { buildScriptBullets, DEMO_PERSONAS, storeView } from "./shared.ts";
3
+ import type { RetailState } from "./shared.ts";
4
+ import { buildScriptBullets, DEMO_PERSONAS, emptyRetailState, storeView } from "./shared.ts";
5
+ import { seedStore } from "./store.ts";
5
6
 
6
7
  function makeState(authenticatedUserId: string | null): RetailState {
7
8
  return {
8
- store: structuredClone(seedJson) as unknown as Store,
9
+ store: seedStore(),
9
10
  authenticatedUserId,
10
11
  callSeq: 3,
11
12
  activity: [{ seq: 3, tool: "get_order_details", summary: "read #W5866402", at: 0 }],
@@ -15,20 +16,21 @@ function makeState(authenticatedUserId: string | null): RetailState {
15
16
 
16
17
  describe("storeView", () => {
17
18
  test("projects nothing identifying before authentication", () => {
18
- const view = storeView({ retail: makeState(null) });
19
+ const view = storeView(makeState(null));
19
20
  expect(view.customer).toBeNull();
20
21
  expect(view.orders).toEqual([]);
21
22
  });
22
23
 
23
- test("an absent state slot degrades to an empty view", () => {
24
- const view = storeView({});
24
+ test("an untouched session projects an empty view, not undefined", () => {
25
+ // The value `client.tsx` hoists as its pre-first-tool-call fallback.
26
+ const view = storeView(emptyRetailState());
25
27
  expect(view.customer).toBeNull();
26
28
  expect(view.orders).toEqual([]);
27
29
  expect(view.callSeq).toBe(0);
28
30
  });
29
31
 
30
32
  test("projects the authenticated customer and only their orders", () => {
31
- const view = storeView({ retail: makeState("olivia_ito_3591") });
33
+ const view = storeView(makeState("olivia_ito_3591"));
32
34
  expect(view.customer?.name).toBe("Olivia Ito");
33
35
  expect(view.customer?.email).toBe("olivia.ito5204@example.com");
34
36
  expect(view.orders.map((o) => o.orderId).sort((a, b) => a.localeCompare(b))).toEqual(
@@ -40,13 +42,13 @@ describe("storeView", () => {
40
42
 
41
43
  test("projects live gift-card balances, since refunds move them", () => {
42
44
  const state = makeState("olivia_ito_3591");
43
- const view = storeView({ retail: state });
45
+ const view = storeView(state);
44
46
  const card = view.customer?.paymentMethods.find((m) => m.id === "gift_card_7794233");
45
47
  expect(card?.balance).toBe(56);
46
48
  });
47
49
 
48
50
  test("no other customer's email, address, or payment method reaches the client", () => {
49
- const view = storeView({ retail: makeState("olivia_ito_3591") });
51
+ const view = storeView(makeState("olivia_ito_3591"));
50
52
  const json = JSON.stringify(view);
51
53
  for (const leaked of [
52
54
  "aarav.anderson9752@example.com",
@@ -64,14 +66,14 @@ describe("storeView", () => {
64
66
  });
65
67
 
66
68
  test("carries callSeq, activity and focus through", () => {
67
- const view = storeView({ retail: makeState("olivia_ito_3591") });
69
+ const view = storeView(makeState("olivia_ito_3591"));
68
70
  expect(view.callSeq).toBe(3);
69
71
  expect(view.activity).toHaveLength(1);
70
72
  expect(view.focus.orderId).toBe("#W5866402");
71
73
  });
72
74
 
73
75
  test("order totals sum the payments, not the items", () => {
74
- const view = storeView({ retail: makeState("olivia_ito_3591") });
76
+ const view = storeView(makeState("olivia_ito_3591"));
75
77
  const delivered = view.orders.find((o) => o.orderId === "#W5866402");
76
78
  expect(delivered?.total).toBe(3203.78);
77
79
  });
@@ -83,7 +85,7 @@ describe("DEMO_PERSONAS", () => {
83
85
  });
84
86
 
85
87
  test("every persona's email, name and zip match its seed record", () => {
86
- const users = Object.values((structuredClone(seedJson) as unknown as Store).users);
88
+ const users = Object.values(seedStore().users);
87
89
  for (const persona of DEMO_PERSONAS) {
88
90
  const user = users.find((u) => u.email === persona.email);
89
91
  expect.soft(user, `no seeded customer has email ${persona.email}`).toBeDefined();
@@ -105,12 +107,12 @@ describe("swapOptions", () => {
105
107
  test("empty when no order is focused", () => {
106
108
  const state = makeState("olivia_ito_3591");
107
109
  state.focus = {};
108
- expect(storeView({ retail: state }).swapOptions).toEqual([]);
110
+ expect(storeView(state).swapOptions).toEqual([]);
109
111
  });
110
112
 
111
113
  test("lists available alternatives for the focused order's items", () => {
112
114
  // makeState focuses #W5866402 — Espresso Machine + Sneakers.
113
- const view = storeView({ retail: makeState("olivia_ito_3591") });
115
+ const view = storeView(makeState("olivia_ito_3591"));
114
116
  expect(view.swapOptions).toHaveLength(2);
115
117
  const espresso = view.swapOptions.find((s) => s.itemId === "6242772310");
116
118
  expect(espresso?.itemName).toBe("Espresso Machine");
@@ -121,7 +123,7 @@ describe("swapOptions", () => {
121
123
  test("never offers an unavailable variant as a target", () => {
122
124
  const state = makeState("aarav_anderson_8794");
123
125
  state.focus = { orderId: "#W4316152" };
124
- const view = storeView({ retail: state });
126
+ const view = storeView(state);
125
127
  const ids = view.swapOptions.flatMap((s) => s.alternatives.map((a) => a.itemId));
126
128
  // 6454334990 is a Tea Kettle variant with available: false.
127
129
  expect(ids).not.toContain("6454334990");
@@ -130,7 +132,7 @@ describe("swapOptions", () => {
130
132
  test("caps both axes so the payload stays bounded", () => {
131
133
  const state = makeState("aarav_anderson_8794");
132
134
  state.focus = { orderId: "#W9311069" }; // five items
133
- const view = storeView({ retail: state });
135
+ const view = storeView(state);
134
136
  expect(view.swapOptions.length).toBeLessThanOrEqual(3);
135
137
  for (const option of view.swapOptions) {
136
138
  expect(option.alternatives.length).toBeLessThanOrEqual(4);
@@ -140,7 +142,7 @@ describe("swapOptions", () => {
140
142
  test("a focus on another customer's order projects nothing", () => {
141
143
  const state = makeState("olivia_ito_3591");
142
144
  state.focus = { orderId: "#W4316152" }; // aarav's
143
- const view = storeView({ retail: state });
145
+ const view = storeView(state);
144
146
  expect(view.swapOptions).toEqual([]);
145
147
  expect(JSON.stringify(view)).not.toContain("#W4316152");
146
148
  });
@@ -153,8 +155,8 @@ describe("buildScriptBullets", () => {
153
155
  expect(bullets.join(" ")).toContain(DEMO_PERSONAS[0]?.zip ?? "");
154
156
  });
155
157
 
156
- test("undefined state yields the pre-auth bullets", () => {
157
- expect(buildScriptBullets(undefined).length).toBeGreaterThan(0);
158
+ test("an empty state yields the pre-auth bullets", () => {
159
+ expect(buildScriptBullets(emptyRetailState()).length).toBeGreaterThan(0);
158
160
  });
159
161
 
160
162
  test("a customer with pending orders is offered cancel and address change", () => {
@@ -1,3 +1,5 @@
1
+ import type { DeepReadonly } from "@alexkroman1/aai";
2
+
1
3
  // ─── Store types ─────────────────────────────────────────────────────────────
2
4
  // Field names are tau2's snake_case verbatim, because `seed.json` is tau2's
3
5
  // records unmodified. Renaming them here would mean transforming 107 KB of
@@ -129,7 +131,36 @@ export interface RetailState {
129
131
  focus: { orderId?: string; productId?: string };
130
132
  }
131
133
 
132
- export type StateSlot = { retail?: RetailState };
134
+ /**
135
+ * The store as a READ hands it out: deep-frozen, and typed to say so.
136
+ *
137
+ * The projection and its helpers take this rather than {@link RetailState},
138
+ * which is the widening a deep-readonly slot forces and the reason it is worth
139
+ * doing: a mutable state still satisfies it, so `client.tsx` still projects
140
+ * `emptyRetailState()` and every `retailTool` body still passes its draft,
141
+ * while a projection helper that WOULD have mutated stops compiling instead of
142
+ * throwing at its first call.
143
+ */
144
+ export type FrozenRetailState = DeepReadonly<RetailState>;
145
+
146
+ /**
147
+ * A pristine, EMPTY store — no seed data.
148
+ *
149
+ * `store.ts`'s `createDefaultState` builds the real seeded state on top of
150
+ * this, so the shape is declared once. It lives here, apart from that, because
151
+ * `client.tsx` needs a `RetailState` for its pre-first-call fallback and must
152
+ * not import `store.ts`: that module pulls the 107 KB `seed.json`, which would
153
+ * then land in the browser bundle.
154
+ */
155
+ export function emptyRetailState(): RetailState {
156
+ return {
157
+ store: { users: {}, orders: {}, products: {} },
158
+ authenticatedUserId: null,
159
+ callSeq: 0,
160
+ activity: [],
161
+ focus: {},
162
+ };
163
+ }
133
164
 
134
165
  // ─── View types — exactly what crosses the wire ──────────────────────────────
135
166
 
@@ -249,13 +280,13 @@ export const DEMO_PERSONAS: readonly DemoPersona[] = [
249
280
 
250
281
  // ─── Projection ──────────────────────────────────────────────────────────────
251
282
 
252
- function paymentLabel(method: PaymentMethod): string {
283
+ function paymentLabel(method: DeepReadonly<PaymentMethod>): string {
253
284
  if (method.source === "gift_card") return "Gift card";
254
285
  if (method.source === "paypal") return "PayPal";
255
286
  return `${method.brand} ending ${method.last_four}`;
256
287
  }
257
288
 
258
- function paymentMethodView(method: PaymentMethod): PaymentMethodView {
289
+ function paymentMethodView(method: DeepReadonly<PaymentMethod>): PaymentMethodView {
259
290
  return {
260
291
  id: method.id,
261
292
  source: method.source,
@@ -267,7 +298,7 @@ function paymentMethodView(method: PaymentMethod): PaymentMethodView {
267
298
  /** Net of payments minus refunds — what the customer has actually paid. Items
268
299
  * can't be summed instead: a modified order's items no longer match what was
269
300
  * charged. */
270
- export function orderTotal(order: Order): number {
301
+ export function orderTotal(order: DeepReadonly<Order>): number {
271
302
  const net = order.payment_history.reduce(
272
303
  (sum, p) => sum + (p.transaction_type === "payment" ? p.amount : -p.amount),
273
304
  0,
@@ -275,7 +306,7 @@ export function orderTotal(order: Order): number {
275
306
  return Math.round(net * 100) / 100;
276
307
  }
277
308
 
278
- function orderView(order: Order): OrderView {
309
+ function orderView(order: DeepReadonly<Order>): OrderView {
279
310
  return {
280
311
  orderId: order.order_id,
281
312
  status: order.status,
@@ -295,7 +326,10 @@ function orderView(order: Order): OrderView {
295
326
  * an exchange sayable. Only available variants, only the focused order, and
296
327
  * capped on both axes so the payload stays bounded.
297
328
  */
298
- function swapOptionsFor(state: RetailState, order: Order | undefined): SwapOptionView[] {
329
+ function swapOptionsFor(
330
+ state: FrozenRetailState,
331
+ order: DeepReadonly<Order> | undefined,
332
+ ): SwapOptionView[] {
299
333
  if (!order) return [];
300
334
  return order.items.slice(0, MAX_SWAP_ITEMS).map((item) => {
301
335
  const product = state.store.products[item.product_id];
@@ -321,16 +355,20 @@ function swapOptionsFor(state: RetailState, order: Order | undefined): SwapOptio
321
355
  *
322
356
  * Only the authenticated customer is projected. The other five seeded
323
357
  * customers' emails, addresses, and payment methods never leave the server,
324
- * which is why `syncState` takes a projection rather than a boolean. */
325
- export function storeView(slot: StateSlot): StoreView {
326
- const state = slot.retail;
327
- const userId = state?.authenticatedUserId ?? null;
328
- const user = userId ? state?.store.users[userId] : undefined;
329
- const focusedOrderId = state?.focus.orderId;
358
+ * which is why `syncState` takes a projection rather than a boolean.
359
+ *
360
+ * Takes the state itself, not the slot: `retailSlot.projection` supplies a
361
+ * real one even before the first tool call, so the optional chaining this
362
+ * used to carry which read as security gating and was not — is gone. The
363
+ * gating that IS security is still here, on `user`. */
364
+ export function storeView(state: FrozenRetailState): StoreView {
365
+ const userId = state.authenticatedUserId;
366
+ const user = userId ? state.store.users[userId] : undefined;
367
+ const focusedOrderId = state.focus.orderId;
330
368
  // Gated on `user`: an order is only ever projected for the customer on the
331
369
  // call, so a stale focus from before authentication cannot leak one.
332
370
  const focusedOrder =
333
- state && user && focusedOrderId && user.orders.includes(focusedOrderId)
371
+ user && focusedOrderId && user.orders.includes(focusedOrderId)
334
372
  ? state.store.orders[focusedOrderId]
335
373
  : undefined;
336
374
 
@@ -346,7 +384,7 @@ export function storeView(slot: StateSlot): StoreView {
346
384
  : null,
347
385
  orders: user
348
386
  ? user.orders
349
- .map((id) => state?.store.orders[id])
387
+ .map((id) => state.store.orders[id])
350
388
  .filter((o): o is Order => o !== undefined)
351
389
  .map(orderView)
352
390
  : [],
@@ -356,13 +394,13 @@ export function storeView(slot: StateSlot): StoreView {
356
394
  // though its details were already withheld from `orders`/`swapOptions`.
357
395
  focus: {
358
396
  ...(focusedOrder && focusedOrderId ? { orderId: focusedOrderId } : {}),
359
- ...(state?.focus.productId ? { productId: state.focus.productId } : {}),
397
+ ...(state.focus.productId ? { productId: state.focus.productId } : {}),
360
398
  },
361
- swapOptions: state ? swapOptionsFor(state, focusedOrder) : [],
362
- callSeq: state?.callSeq ?? 0,
363
- activity: state?.activity.slice(-MAX_ACTIVITY) ?? [],
399
+ swapOptions: swapOptionsFor(state, focusedOrder),
400
+ callSeq: state.callSeq,
401
+ activity: state.activity.slice(-MAX_ACTIVITY),
364
402
  scriptBullets: buildScriptBullets(state),
365
- productCount: Object.keys(state?.store.products ?? {}).length,
403
+ productCount: Object.keys(state.store.products).length,
366
404
  };
367
405
  }
368
406
 
@@ -375,13 +413,13 @@ const MAX_BULLETS = 6;
375
413
  * that offers actions the data can't support — "return an item" to someone
376
414
  * with no delivered order — is worse than no list.
377
415
  */
378
- export function buildScriptBullets(state: RetailState | undefined): string[] {
379
- const userId = state?.authenticatedUserId;
380
- const user = userId ? state?.store.users[userId] : undefined;
416
+ export function buildScriptBullets(state: FrozenRetailState): string[] {
417
+ const userId = state.authenticatedUserId;
418
+ const user = userId ? state.store.users[userId] : undefined;
381
419
 
382
420
  // Pre-auth: only the two ways in. The personas panel (a constant, rendered
383
421
  // straight from DEMO_PERSONAS) covers WHICH customer to be.
384
- if (!(state && user)) {
422
+ if (!user) {
385
423
  const first = DEMO_PERSONAS[0];
386
424
  return [
387
425
  `"My email is ${first?.email ?? ""}"`,
@@ -1,4 +1,5 @@
1
- import type { ToolContext } from "@alexkroman1/aai";
1
+ import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
2
+ import { createToolContext } from "@alexkroman1/aai/testing";
2
3
  import { describe, expect, test } from "vitest";
3
4
  import { z } from "zod";
4
5
  import {
@@ -10,31 +11,22 @@ import {
10
11
  findProduct,
11
12
  findUser,
12
13
  findVariant,
13
- getState,
14
- isError,
15
14
  isGiftCard,
16
15
  money,
17
16
  requireOwnOrder,
17
+ retailSlot,
18
18
  retailTool,
19
19
  } from "./store.ts";
20
20
 
21
- let sessionCounter = 0;
22
-
23
21
  function makeCtx(): ToolContext {
24
- return {
25
- sessionId: `test-session-${++sessionCounter}`,
26
- send: () => {},
27
- env: {},
28
- state: {},
29
- messages: [],
30
- } as unknown as ToolContext;
22
+ return createToolContext();
31
23
  }
32
24
 
33
25
  describe("session state", () => {
34
- test("getState seeds lazily and returns the same object on re-entry", () => {
26
+ test("the slot seeds lazily and returns the same object on re-entry", () => {
35
27
  const ctx = makeCtx();
36
- const a = getState(ctx);
37
- const b = getState(ctx);
28
+ const a = retailSlot.get(ctx);
29
+ const b = retailSlot.get(ctx);
38
30
  expect(a).toBe(b);
39
31
  expect(a.authenticatedUserId).toBeNull();
40
32
  expect(a.callSeq).toBe(0);
@@ -42,14 +34,15 @@ describe("session state", () => {
42
34
  });
43
35
 
44
36
  test("each session gets its own deep copy — a mutation cannot leak across sessions", () => {
45
- const first = getState(makeCtx());
46
- const order = first.store.orders["#W9300146"];
47
- if (!order) throw new Error("fixture missing");
48
- order.status = "cancelled";
49
- const giftCard = first.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
50
- if (giftCard?.source === "gift_card") giftCard.balance = 0;
51
-
52
- const second = getState(makeCtx());
37
+ retailSlot.update(makeCtx(), (first) => {
38
+ const order = first.store.orders["#W9300146"];
39
+ if (!order) throw new Error("fixture missing");
40
+ order.status = "cancelled";
41
+ const giftCard = first.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
42
+ if (giftCard?.source === "gift_card") giftCard.balance = 0;
43
+ });
44
+
45
+ const second = retailSlot.get(makeCtx());
53
46
  expect(second.store.orders["#W9300146"]?.status).toBe("pending");
54
47
  const fresh = second.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
55
48
  expect(fresh?.source === "gift_card" && fresh.balance).toBe(17);
@@ -67,52 +60,52 @@ describe("money", () => {
67
60
  describe("lookups", () => {
68
61
  test("findUser resolves and reports a miss by id", () => {
69
62
  const state = createDefaultState();
70
- expect(isError(findUser(state, "olivia_ito_3591"))).toBe(false);
63
+ expect(isToolFailure(findUser(state, "olivia_ito_3591"))).toBe(false);
71
64
  const miss = findUser(state, "nobody_1");
72
- expect(isError(miss) && miss.error).toContain("nobody_1");
65
+ expect(isToolFailure(miss) && miss.error).toContain("nobody_1");
73
66
  });
74
67
 
75
68
  test("findOrder resolves and reports a miss", () => {
76
69
  const state = createDefaultState();
77
70
  const order = findOrder(state, "#W5866402");
78
- expect(isError(order)).toBe(false);
79
- expect(isError(order) ? null : order.status).toBe("delivered");
80
- expect(isError(findOrder(state, "#W0000000"))).toBe(true);
71
+ expect(isToolFailure(order)).toBe(false);
72
+ expect(isToolFailure(order) ? null : order.status).toBe("delivered");
73
+ expect(isToolFailure(findOrder(state, "#W0000000"))).toBe(true);
81
74
  });
82
75
 
83
76
  test("findProduct and findVariant resolve within one product", () => {
84
77
  const state = createDefaultState();
85
78
  const product = findProduct(state, "9832717871");
86
- if (isError(product)) throw new Error(product.error);
79
+ if (isToolFailure(product)) throw new Error(product.error);
87
80
  expect(product.name).toBe("Tea Kettle");
88
81
  const variant = findVariant(product, "3909406921");
89
- expect(isError(variant) ? null : variant.price).toBe(98.25);
82
+ expect(isToolFailure(variant) ? null : variant.price).toBe(98.25);
90
83
  // A real item id, but of a different product.
91
- expect(isError(findVariant(product, "4725166838"))).toBe(true);
84
+ expect(isToolFailure(findVariant(product, "4725166838"))).toBe(true);
92
85
  });
93
86
 
94
87
  test("findItem scans every product and returns both product and variant", () => {
95
88
  const state = createDefaultState();
96
89
  const found = findItem(state, "3909406921");
97
- if (isError(found)) throw new Error(found.error);
90
+ if (isToolFailure(found)) throw new Error(found.error);
98
91
  expect(found.product.product_id).toBe("9832717871");
99
92
  expect(found.variant.price).toBe(98.25);
100
- expect(isError(findItem(state, "0000000000"))).toBe(true);
93
+ expect(isToolFailure(findItem(state, "0000000000"))).toBe(true);
101
94
  });
102
95
 
103
96
  test("findPaymentMethod resolves on the owning user only", () => {
104
97
  const state = createDefaultState();
105
98
  const olivia = findUser(state, "olivia_ito_3591");
106
- if (isError(olivia)) throw new Error(olivia.error);
107
- expect(isError(findPaymentMethod(olivia, "gift_card_7794233"))).toBe(false);
99
+ if (isToolFailure(olivia)) throw new Error(olivia.error);
100
+ expect(isToolFailure(findPaymentMethod(olivia, "gift_card_7794233"))).toBe(false);
108
101
  // aarav's card is not olivia's.
109
- expect(isError(findPaymentMethod(olivia, "gift_card_7245904"))).toBe(true);
102
+ expect(isToolFailure(findPaymentMethod(olivia, "gift_card_7245904"))).toBe(true);
110
103
  });
111
104
 
112
105
  test("isGiftCard narrows", () => {
113
106
  const state = createDefaultState();
114
107
  const olivia = findUser(state, "olivia_ito_3591");
115
- if (isError(olivia)) throw new Error(olivia.error);
108
+ if (isToolFailure(olivia)) throw new Error(olivia.error);
116
109
  const card = olivia.payment_methods.gift_card_7794233;
117
110
  const paypal = olivia.payment_methods.paypal_8049766;
118
111
  expect(card && isGiftCard(card)).toBe(true);
@@ -124,15 +117,15 @@ describe("ownership and authentication guards", () => {
124
117
  test("authenticatedUser errors before authentication", () => {
125
118
  const state = createDefaultState();
126
119
  const result = authenticatedUser(state);
127
- expect(isError(result) && result.error.toLowerCase()).toContain("find_user_id_by_email");
120
+ expect(isToolFailure(result) && result.error.toLowerCase()).toContain("find_user_id_by_email");
128
121
  });
129
122
 
130
123
  test("requireOwnOrder refuses another customer's order without revealing it", () => {
131
124
  const state = createDefaultState();
132
125
  state.authenticatedUserId = "olivia_ito_3591";
133
126
  const result = requireOwnOrder(state, "#W4316152"); // aarav's
134
- expect(isError(result)).toBe(true);
135
- if (!isError(result)) throw new Error("expected refusal");
127
+ expect(isToolFailure(result)).toBe(true);
128
+ if (!isToolFailure(result)) throw new Error("expected refusal");
136
129
  expect(result.error).not.toContain("aarav");
137
130
  expect(result.error).not.toContain("Tea Kettle");
138
131
  });
@@ -144,8 +137,8 @@ describe("ownership and authentication guards", () => {
144
137
  const missingId = "#W0000000";
145
138
  const foreign = requireOwnOrder(state, foreignId);
146
139
  const missing = requireOwnOrder(state, missingId);
147
- expect(isError(foreign) && isError(missing)).toBe(true);
148
- if (!(isError(foreign) && isError(missing))) throw new Error("expected refusals");
140
+ expect(isToolFailure(foreign) && isToolFailure(missing)).toBe(true);
141
+ if (!(isToolFailure(foreign) && isToolFailure(missing))) throw new Error("expected refusals");
149
142
  // Structural, not literal, equality: the messages legitimately differ by
150
143
  // the id the caller themselves passed in (that's not information about
151
144
  // the store — they already knew it), so comparing byte-for-byte would be
@@ -161,7 +154,7 @@ describe("ownership and authentication guards", () => {
161
154
  const state = createDefaultState();
162
155
  state.authenticatedUserId = "olivia_ito_3591";
163
156
  const result = requireOwnOrder(state, "#W5866402");
164
- expect(isError(result) ? null : result.order_id).toBe("#W5866402");
157
+ expect(isToolFailure(result) ? null : result.order_id).toBe("#W5866402");
165
158
  });
166
159
  });
167
160
 
@@ -196,7 +189,7 @@ describe("retailTool", () => {
196
189
  const ctx = makeCtx();
197
190
  await echo.execute({ value: "a" }, ctx);
198
191
  await echo.execute({ value: "b" }, ctx);
199
- const state = getState(ctx);
192
+ const state = retailSlot.get(ctx);
200
193
  expect(state.callSeq).toBe(2);
201
194
  expect(state.activity.map((a) => a.summary)).toEqual(["echoed a", "echoed b"]);
202
195
  expect(state.activity.map((a) => a.tool)).toEqual(["echo", "echo"]);
@@ -205,28 +198,30 @@ describe("retailTool", () => {
205
198
  test("a repeated identical call still changes the projection", async () => {
206
199
  const ctx = makeCtx();
207
200
  await echo.execute({ value: "same" }, ctx);
208
- const first = getState(ctx).callSeq;
201
+ const first = retailSlot.get(ctx).callSeq;
209
202
  await echo.execute({ value: "same" }, ctx);
210
- expect(getState(ctx).callSeq).toBeGreaterThan(first);
203
+ expect(retailSlot.get(ctx).callSeq).toBeGreaterThan(first);
211
204
  });
212
205
 
213
206
  test("defaults to requiring authentication and does not run execute when blocked", async () => {
214
207
  const ctx = makeCtx();
215
208
  const result = await gated.execute({}, ctx);
216
- expect(isError(result) && result.error).toContain("find_user_id_by_email");
209
+ expect(isToolFailure(result) && result.error).toContain("find_user_id_by_email");
217
210
  });
218
211
 
219
212
  test("a blocked call is still logged and still bumps callSeq", async () => {
220
213
  const ctx = makeCtx();
221
214
  await gated.execute({}, ctx);
222
- const state = getState(ctx);
215
+ const state = retailSlot.get(ctx);
223
216
  expect(state.callSeq).toBe(1);
224
217
  expect(state.activity[0]?.summary).toContain("blocked");
225
218
  });
226
219
 
227
220
  test("runs once authenticated", async () => {
228
221
  const ctx = makeCtx();
229
- getState(ctx).authenticatedUserId = "olivia_ito_3591";
222
+ retailSlot.update(ctx, (state) => {
223
+ state.authenticatedUserId = "olivia_ito_3591";
224
+ });
230
225
  const result = await gated.execute({}, ctx);
231
226
  expect(result).toEqual({ ok: true });
232
227
  });
@@ -234,13 +229,13 @@ describe("retailTool", () => {
234
229
  test("an error result is logged as an error, not through summary()", async () => {
235
230
  const ctx = makeCtx();
236
231
  await failing.execute({}, ctx);
237
- expect(getState(ctx).activity[0]?.summary).toBe("error: nope");
232
+ expect(retailSlot.get(ctx).activity[0]?.summary).toBe("error: nope");
238
233
  });
239
234
 
240
235
  test("activity is capped so a long call cannot grow the payload", async () => {
241
236
  const ctx = makeCtx();
242
237
  for (let i = 0; i < 15; i++) await echo.execute({ value: String(i) }, ctx);
243
- const state = getState(ctx);
238
+ const state = retailSlot.get(ctx);
244
239
  expect(state.callSeq).toBe(15);
245
240
  expect(state.activity).toHaveLength(10);
246
241
  expect(state.activity[0]?.summary).toBe("echoed 5");
@@ -249,7 +244,7 @@ describe("retailTool", () => {
249
244
  test("concurrent calls serialize — no lost increments", async () => {
250
245
  const ctx = makeCtx();
251
246
  await Promise.all(Array.from({ length: 8 }, (_, i) => echo.execute({ value: String(i) }, ctx)));
252
- const state = getState(ctx);
247
+ const state = retailSlot.get(ctx);
253
248
  expect(state.callSeq).toBe(8);
254
249
  expect(new Set(state.activity.map((a) => a.seq)).size).toBe(state.activity.length);
255
250
  });