@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,22 +1,6 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
2
  import { storeView } from "./shared.ts";
3
- import { retailSlot, type StateSlot } from "./store.ts";
4
- import systemPrompt from "./system-prompt.md?raw";
5
- import { cancelPendingOrder } from "./tools/cancel_pending_order.ts";
6
- import { exchangeDeliveredOrderItems } from "./tools/exchange_delivered_order_items.ts";
7
- import { findUserIdByEmail } from "./tools/find_user_id_by_email.ts";
8
- import { findUserIdByNameZip } from "./tools/find_user_id_by_name_zip.ts";
9
- import { getItemDetails } from "./tools/get_item_details.ts";
10
- import { getOrderDetails } from "./tools/get_order_details.ts";
11
- import { getProductDetails } from "./tools/get_product_details.ts";
12
- import { getUserDetails } from "./tools/get_user_details.ts";
13
- import { listAllProductTypes } from "./tools/list_all_product_types.ts";
14
- import { modifyPendingOrderAddress } from "./tools/modify_pending_order_address.ts";
15
- import { modifyPendingOrderItems } from "./tools/modify_pending_order_items.ts";
16
- import { modifyPendingOrderPayment } from "./tools/modify_pending_order_payment.ts";
17
- import { modifyUserAddress } from "./tools/modify_user_address.ts";
18
- import { returnDeliveredOrderItems } from "./tools/return_delivered_order_items.ts";
19
- import { transferToHumanAgents } from "./tools/transfer_to_human_agents.ts";
3
+ import { retailSlot } from "./store.ts";
20
4
 
21
5
  export default agent({
22
6
  name: "Retail Support",
@@ -25,12 +9,10 @@ export default agent({
25
9
  // filled from the all-AssemblyAI pipeline at parse time. Only `stt` is
26
10
  // overridden below; `llm` and `tts` take the defaults.
27
11
 
28
- // The store lives in ctx.state, one pristine copy per session — callers must
29
- // not see each other's cancellations. Declaring `state` (rather than letting
30
- // the slot install itself on first access) means the session's store exists
31
- // before the first tool call, so a resumed connection has something to
32
- // project; the slot owns the shape either way.
33
- state: (): StateSlot => ({ [retailSlot.key]: retailSlot.create() }),
12
+ // The store lives in one `sessionSlot` (`store.ts`), a pristine copy per
13
+ // session — callers must not see each other's cancellations. Nothing declares
14
+ // it here: the slot installs itself on first access, and its `projection`
15
+ // below is what gives a session that has run no tool something to render.
34
16
 
35
17
  // One projection pushed after every tool call. It is a projection, not a
36
18
  // flag, because the state holds all six seeded customers and only the
@@ -41,26 +23,7 @@ export default agent({
41
23
  // inside one utterance ("W seven six seven … eight oh seven two"). The default
42
24
  // pipeline's `max_turn_silence` already tolerates that; reach for
43
25
  // `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
44
- systemPrompt,
45
26
  greeting:
46
27
  "Thanks for calling. Before I can look anything up I'll need to find your account — " +
47
28
  "what's the email address on it?",
48
-
49
- tools: {
50
- cancel_pending_order: cancelPendingOrder,
51
- exchange_delivered_order_items: exchangeDeliveredOrderItems,
52
- find_user_id_by_email: findUserIdByEmail,
53
- find_user_id_by_name_zip: findUserIdByNameZip,
54
- get_item_details: getItemDetails,
55
- get_order_details: getOrderDetails,
56
- get_product_details: getProductDetails,
57
- get_user_details: getUserDetails,
58
- list_all_product_types: listAllProductTypes,
59
- modify_pending_order_address: modifyPendingOrderAddress,
60
- modify_pending_order_items: modifyPendingOrderItems,
61
- modify_pending_order_payment: modifyPendingOrderPayment,
62
- modify_user_address: modifyUserAddress,
63
- return_delivered_order_items: returnDeliveredOrderItems,
64
- transfer_to_human_agents: transferToHumanAgents,
65
- },
66
29
  });
@@ -1,6 +1,12 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { ChatMessage } from "@alexkroman1/aai-ui";
3
- import { AutoScroll, client, useAgentState, useSession } from "@alexkroman1/aai-ui";
2
+ import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
3
+ import {
4
+ AutoScroll,
5
+ client,
6
+ useAgentState,
7
+ useSession,
8
+ useUserTranscript,
9
+ } from "@alexkroman1/aai-ui";
4
10
  import type { ReactNode } from "react";
5
11
  import type {
6
12
  OrderStatus,
@@ -43,12 +49,24 @@ const statusColors: Record<string, string> = {
43
49
  // importing it here would ship the whole catalog to the browser.
44
50
  const EMPTY_VIEW: StoreView = storeView(emptyRetailState());
45
51
 
46
- function stateColor(state: string): string {
47
- if (state === "listening" || state === "ready") return "#16a34a";
48
- if (state === "thinking") return "#ca8a04";
49
- if (state === "speaking") return "#2563eb";
50
- return "#a1a1aa";
51
- }
52
+ // The dot's colour per session state, as an EXHAUSTIVE map rather than an
53
+ // if-chain with a grey default.
54
+ //
55
+ // The palette is this template's own — every client here paints the same six
56
+ // states in its own colours, so the SDK has nothing to share but the union. What
57
+ // the SDK does own is `AgentState`, and `satisfies Record<AgentState, string>`
58
+ // is what borrows it: a state added there stops compiling here, where the
59
+ // `state === "…"` chain this replaced answered a new state with a silent grey
60
+ // badge in three separate files and no way to notice.
61
+ const STATE_COLORS = {
62
+ disconnected: "#a1a1aa",
63
+ connecting: "#a1a1aa",
64
+ ready: "#16a34a",
65
+ listening: "#16a34a",
66
+ thinking: "#ca8a04",
67
+ speaking: "#2563eb",
68
+ error: "#a1a1aa",
69
+ } satisfies Record<AgentState, string>;
52
70
 
53
71
  function Panel({ title, children }: { title: string; children: ReactNode }) {
54
72
  return (
@@ -195,6 +213,10 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
195
213
 
196
214
  function App() {
197
215
  const session = useSession();
216
+ // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
217
+ // is silence and `""` is speech detected with no words back yet, and reading
218
+ // them as one falsy value hides the indicator at the start of every turn.
219
+ const { speaking, text: userTranscript } = useUserTranscript();
198
220
  // The agent's own store, projected by `syncState` after every tool call.
199
221
  const view = useAgentState<StoreView>(EMPTY_VIEW);
200
222
 
@@ -217,7 +239,7 @@ function App() {
217
239
  <span
218
240
  className="w-2 h-2 rounded-full inline-block"
219
241
  style={{
220
- background: stateColor(session.state),
242
+ background: STATE_COLORS[session.state],
221
243
  animation:
222
244
  session.state === "listening"
223
245
  ? "rt-pulse 1.5s ease-in-out infinite"
@@ -279,7 +301,7 @@ function App() {
279
301
  ))}
280
302
  </AutoScroll>
281
303
 
282
- {session.userTranscript !== null && (
304
+ {speaking && (
283
305
  <div
284
306
  className="flex items-center px-4 py-2 text-xs italic min-h-8"
285
307
  style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
@@ -288,7 +310,7 @@ function App() {
288
310
  className="w-2 h-2 rounded-full inline-block mr-2"
289
311
  style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
290
312
  />
291
- {session.userTranscript === "" ? "…" : session.userTranscript}
313
+ {userTranscript}
292
314
  </div>
293
315
  )}
294
316
  {session.error && (
@@ -1,6 +1,9 @@
1
+ /// <reference types="vite/client" />
2
+
1
3
  import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
4
+ import { createToolContext, withDiscoveredTools } from "@alexkroman1/aai/testing";
2
5
  import { describe, expect, test } from "vitest";
3
- import retailAgent from "./agent.ts";
6
+ import authoredAgent from "./agent.ts";
4
7
  import { retailSlot } from "./store.ts";
5
8
 
6
9
  /** Tools that legitimately run before the caller is identified. Everything
@@ -15,18 +18,24 @@ const PUBLIC_TOOLS = new Set([
15
18
  "transfer_to_human_agents",
16
19
  ]);
17
20
 
18
- const registry = Object.entries(retailAgent.tools ?? {});
21
+ /**
22
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
23
+ *
24
+ * The glob is written HERE rather than reached for from a shared helper because
25
+ * this file SHIPS: it is what a scaffolded project runs, so it may not import
26
+ * anything outside its own template, and `import.meta.glob` is expanded against
27
+ * the file containing it either way. This is the pattern a user writes.
28
+ */
29
+ const retailAgent = withDiscoveredTools(
30
+ authoredAgent,
31
+ import.meta.glob("./tools/*.ts", { eager: true }),
32
+ );
33
+ const registry = Object.entries(retailAgent.tools);
19
34
 
20
- let sessionCounter = 0;
21
- function makeCtx(): ToolContext {
22
- return {
23
- sessionId: `registry-test-${++sessionCounter}`,
24
- send: () => {},
25
- env: {},
26
- state: {},
27
- messages: [],
28
- } as unknown as ToolContext;
29
- }
35
+ // `createToolContext()` rather than a cast: it carries a real slot store (the
36
+ // same storability check and freeze the deployed one applies), and each call is a
37
+ // distinct session, which is what these per-tool cases assume.
38
+ const makeCtx = (): ToolContext => createToolContext();
30
39
 
31
40
  /** Minimal args satisfying each tool's schema. Deliberately plausible-shaped
32
41
  * but wrong — these calls are expected to fail; what is asserted is that they
@@ -175,10 +184,16 @@ describe("agent config", () => {
175
184
  expect(typeof retailAgent.syncState).toBe("function");
176
185
  });
177
186
 
178
- test("declares a state factory, so sessions do not share a store", () => {
179
- expect(typeof retailAgent.state).toBe("function");
180
- const a = retailAgent.state?.();
181
- const b = retailAgent.state?.();
187
+ // The slot owns the default now there is no `state` factory on the agent to
188
+ // forget to declare, which is what four of five slot-backed templates used to.
189
+ // What still has to hold is that the factory really is called per session: the
190
+ // 107 KB seed is one module-level object, so a shared clone would let one
191
+ // caller's cancellation show up in another's.
192
+ test("the slot's factory clones the seed, so sessions do not share a store", () => {
193
+ const a = retailSlot.create();
194
+ const b = retailSlot.create();
182
195
  expect(a).not.toBe(b);
196
+ expect(a.store.orders).not.toBe(b.store.orders);
197
+ expect(a.store).toEqual(b.store);
183
198
  });
184
199
  });
@@ -1,4 +1,15 @@
1
- import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
1
+ /**
2
+ * Spoken references to this store's things.
3
+ *
4
+ * The generic half — reading digits out of an utterance, reading a position out
5
+ * of one, and the never-guess contract that turns a list of candidates into one
6
+ * of them or into a failure that lists them — is `resolveOne`, `spokenDigits`
7
+ * and `spokenOrdinal` (`@alexkroman1/aai`). What stays here is the STORE's
8
+ * vocabulary: what an order id looks like when a caller reads it aloud, which
9
+ * words name a status, and what a variant's options are matched against.
10
+ */
11
+
12
+ import { isToolFailure, resolveOne, spokenDigits, type ToolFailure } from "@alexkroman1/aai";
2
13
  import type { Order, OrderStatus, Product, RetailState } from "./shared.ts";
3
14
  import { authenticatedUser } from "./store.ts";
4
15
 
@@ -10,27 +21,13 @@ export function normalizeOrderId(spoken: string): string {
10
21
 
11
22
  /** Item ids are 10 digits; callers read them in groups. */
12
23
  export function normalizeItemId(spoken: string): string {
13
- return spoken.replace(/\D/g, "");
24
+ return spokenDigits(spoken);
14
25
  }
15
26
 
16
27
  /** A digit run long enough to be an id rather than an ordinal. */
17
28
  const LOOKS_LIKE_ORDER_ID = /\d[\d\s-]{5,}/;
18
29
  const LOOKS_LIKE_ITEM_ID = /(?:\d[\s-]*){8,}/;
19
30
 
20
- const ORDINALS: Record<string, number> = {
21
- first: 0,
22
- "1st": 0,
23
- second: 1,
24
- "2nd": 1,
25
- third: 2,
26
- "3rd": 2,
27
- fourth: 3,
28
- "4th": 3,
29
- fifth: 4,
30
- "5th": 4,
31
- last: -1,
32
- };
33
-
34
31
  /** Spoken status words → the statuses they select. `pending` deliberately
35
32
  * excludes `pending (item modified)`, which no tool will accept anyway. */
36
33
  const STATUS_WORDS: [string, OrderStatus[]][] = [
@@ -72,6 +69,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ToolFa
72
69
  const statusEntry = STATUS_WORDS.find(([word]) => text.includes(word));
73
70
  const candidates = statusEntry ? owned.filter((o) => statusEntry[1].includes(o.status)) : owned;
74
71
 
72
+ // The empty case is the store's to answer, not `resolveOne`'s: "no cancelled
73
+ // order" is only useful said alongside the statuses this customer DOES have,
74
+ // which is what lets the model ask its next question without another tool call.
75
75
  if (candidates.length === 0) {
76
76
  const have = [...new Set(owned.map((o) => o.status))].join(", ");
77
77
  return {
@@ -79,28 +79,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ToolFa
79
79
  };
80
80
  }
81
81
 
82
- const ordinalWord = Object.keys(ORDINALS).find((word) => new RegExp(`\\b${word}\\b`).test(text));
83
- if (ordinalWord) {
84
- const index = ORDINALS[ordinalWord] ?? 0;
85
- const picked = index === -1 ? candidates.at(-1) : candidates[index];
86
- if (!picked) {
87
- return {
88
- error: `There is no ${ordinalWord} such order — the customer has ${candidates.length}: ${candidates
89
- .map(describeOrder)
90
- .join("; ")}.`,
91
- };
92
- }
93
- return picked;
94
- }
95
-
96
- const only = candidates[0];
97
- if (candidates.length === 1 && only) return only;
98
-
99
- return {
100
- error: `That is ambiguous — ${candidates.length} orders match. Ask which one: ${candidates
101
- .map(describeOrder)
102
- .join("; ")}.`,
103
- };
82
+ // A position, then exactly-one, then an ambiguity that lists them — and never
83
+ // a guess, because the consequence here is cancelling the wrong order.
84
+ return resolveOne(candidates, spoken, { label: "order", describe: describeOrder });
104
85
  }
105
86
 
106
87
  /**
@@ -127,33 +108,16 @@ export function resolveVariantId(
127
108
  return itemId;
128
109
  }
129
110
 
130
- const text = spoken.toLowerCase();
131
111
  const pool = Object.values(product.variants).filter((v) => !opts.availableOnly || v.available);
132
112
 
133
- let best = 0;
134
- const scored = pool.map((variant) => {
135
- const score = Object.values(variant.options).filter((value) =>
136
- text.includes(value.toLowerCase()),
137
- ).length;
138
- best = Math.max(best, score);
139
- return { variant, score };
113
+ // A variant is named by its OPTIONS ("the blue medium"), so the score is how
114
+ // many of them the caller said. Everything after that — no match, one match, a
115
+ // tie is `resolveOne`'s, and a tie is answered rather than broken.
116
+ const picked = resolveOne(pool, spoken, {
117
+ label: `${product.name} option`,
118
+ describe: (v) => `${v.item_id} (${Object.values(v.options).join(", ")})`,
119
+ score: (variant, text) =>
120
+ Object.values(variant.options).filter((value) => text.includes(value.toLowerCase())).length,
140
121
  });
141
-
142
- if (best === 0) {
143
- return {
144
- error: `No ${product.name} option matches "${spoken}". Options available: ${pool
145
- .map((v) => `${v.item_id} (${Object.values(v.options).join(", ")})`)
146
- .join("; ")}.`,
147
- };
148
- }
149
-
150
- const winners = scored.filter((s) => s.score === best);
151
- const winner = winners[0]?.variant;
152
- if (winners.length === 1 && winner) return winner.item_id;
153
-
154
- return {
155
- error: `"${spoken}" matches ${winners.length} ${product.name} options. Ask which: ${winners
156
- .map((s) => `${s.variant.item_id} (${Object.values(s.variant.options).join(", ")})`)
157
- .join("; ")}.`,
158
- };
122
+ return isToolFailure(picked) ? picked : picked.item_id;
159
123
  }
@@ -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";
3
+ import type { RetailState } from "./shared.ts";
4
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 }],
@@ -84,7 +85,7 @@ describe("DEMO_PERSONAS", () => {
84
85
  });
85
86
 
86
87
  test("every persona's email, name and zip match its seed record", () => {
87
- const users = Object.values((structuredClone(seedJson) as unknown as Store).users);
88
+ const users = Object.values(seedStore().users);
88
89
  for (const persona of DEMO_PERSONAS) {
89
90
  const user = users.find((u) => u.email === persona.email);
90
91
  expect.soft(user, `no seeded customer has email ${persona.email}`).toBeDefined();
@@ -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,6 +131,18 @@ export interface RetailState {
129
131
  focus: { orderId?: string; productId?: string };
130
132
  }
131
133
 
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
+
132
146
  /**
133
147
  * A pristine, EMPTY store — no seed data.
134
148
  *
@@ -266,13 +280,13 @@ export const DEMO_PERSONAS: readonly DemoPersona[] = [
266
280
 
267
281
  // ─── Projection ──────────────────────────────────────────────────────────────
268
282
 
269
- function paymentLabel(method: PaymentMethod): string {
283
+ function paymentLabel(method: DeepReadonly<PaymentMethod>): string {
270
284
  if (method.source === "gift_card") return "Gift card";
271
285
  if (method.source === "paypal") return "PayPal";
272
286
  return `${method.brand} ending ${method.last_four}`;
273
287
  }
274
288
 
275
- function paymentMethodView(method: PaymentMethod): PaymentMethodView {
289
+ function paymentMethodView(method: DeepReadonly<PaymentMethod>): PaymentMethodView {
276
290
  return {
277
291
  id: method.id,
278
292
  source: method.source,
@@ -284,7 +298,7 @@ function paymentMethodView(method: PaymentMethod): PaymentMethodView {
284
298
  /** Net of payments minus refunds — what the customer has actually paid. Items
285
299
  * can't be summed instead: a modified order's items no longer match what was
286
300
  * charged. */
287
- export function orderTotal(order: Order): number {
301
+ export function orderTotal(order: DeepReadonly<Order>): number {
288
302
  const net = order.payment_history.reduce(
289
303
  (sum, p) => sum + (p.transaction_type === "payment" ? p.amount : -p.amount),
290
304
  0,
@@ -292,7 +306,7 @@ export function orderTotal(order: Order): number {
292
306
  return Math.round(net * 100) / 100;
293
307
  }
294
308
 
295
- function orderView(order: Order): OrderView {
309
+ function orderView(order: DeepReadonly<Order>): OrderView {
296
310
  return {
297
311
  orderId: order.order_id,
298
312
  status: order.status,
@@ -312,7 +326,10 @@ function orderView(order: Order): OrderView {
312
326
  * an exchange sayable. Only available variants, only the focused order, and
313
327
  * capped on both axes so the payload stays bounded.
314
328
  */
315
- function swapOptionsFor(state: RetailState, order: Order | undefined): SwapOptionView[] {
329
+ function swapOptionsFor(
330
+ state: FrozenRetailState,
331
+ order: DeepReadonly<Order> | undefined,
332
+ ): SwapOptionView[] {
316
333
  if (!order) return [];
317
334
  return order.items.slice(0, MAX_SWAP_ITEMS).map((item) => {
318
335
  const product = state.store.products[item.product_id];
@@ -344,7 +361,7 @@ function swapOptionsFor(state: RetailState, order: Order | undefined): SwapOptio
344
361
  * real one even before the first tool call, so the optional chaining this
345
362
  * used to carry — which read as security gating and was not — is gone. The
346
363
  * gating that IS security is still here, on `user`. */
347
- export function storeView(state: RetailState): StoreView {
364
+ export function storeView(state: FrozenRetailState): StoreView {
348
365
  const userId = state.authenticatedUserId;
349
366
  const user = userId ? state.store.users[userId] : undefined;
350
367
  const focusedOrderId = state.focus.orderId;
@@ -396,7 +413,7 @@ const MAX_BULLETS = 6;
396
413
  * that offers actions the data can't support — "return an item" to someone
397
414
  * with no delivered order — is worse than no list.
398
415
  */
399
- export function buildScriptBullets(state: RetailState): string[] {
416
+ export function buildScriptBullets(state: FrozenRetailState): string[] {
400
417
  const userId = state.authenticatedUserId;
401
418
  const user = userId ? state.store.users[userId] : undefined;
402
419
 
@@ -16,11 +16,10 @@ import {
16
16
  requireOwnOrder,
17
17
  retailSlot,
18
18
  retailTool,
19
- type StateSlot,
20
19
  } from "./store.ts";
21
20
 
22
- function makeCtx(): ToolContext<StateSlot> {
23
- return createToolContext<StateSlot>();
21
+ function makeCtx(): ToolContext {
22
+ return createToolContext();
24
23
  }
25
24
 
26
25
  describe("session state", () => {
@@ -35,12 +34,13 @@ describe("session state", () => {
35
34
  });
36
35
 
37
36
  test("each session gets its own deep copy — a mutation cannot leak across sessions", () => {
38
- const first = retailSlot.get(makeCtx());
39
- const order = first.store.orders["#W9300146"];
40
- if (!order) throw new Error("fixture missing");
41
- order.status = "cancelled";
42
- const giftCard = first.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
43
- if (giftCard?.source === "gift_card") giftCard.balance = 0;
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
44
 
45
45
  const second = retailSlot.get(makeCtx());
46
46
  expect(second.store.orders["#W9300146"]?.status).toBe("pending");
@@ -219,7 +219,9 @@ describe("retailTool", () => {
219
219
 
220
220
  test("runs once authenticated", async () => {
221
221
  const ctx = makeCtx();
222
- retailSlot.get(ctx).authenticatedUserId = "olivia_ito_3591";
222
+ retailSlot.update(ctx, (state) => {
223
+ state.authenticatedUserId = "olivia_ito_3591";
224
+ });
223
225
  const result = await gated.execute({}, ctx);
224
226
  expect(result).toEqual({ ok: true });
225
227
  });
@@ -1,5 +1,5 @@
1
1
  import type { ToolContext, ToolFailure } from "@alexkroman1/aai";
2
- import { isToolFailure, pushCapped, type SlotStateOf, sessionSlot, tool } from "@alexkroman1/aai";
2
+ import { isToolFailure, pushCapped, sessionSlot, toolFailure } from "@alexkroman1/aai";
3
3
  import type { z } from "zod";
4
4
  import seedJson from "./seed.json";
5
5
  import type {
@@ -38,16 +38,32 @@ export function isGiftCard(method: PaymentMethod): method is GiftCard {
38
38
  * module-level object shared by every session in the process, so a mutation
39
39
  * without it would let one caller's cancellation show up in another's. */
40
40
  export function createDefaultState(): RetailState {
41
- return { ...emptyRetailState(), store: structuredClone(SEED) };
41
+ return { ...emptyRetailState(), store: seedStore() };
42
42
  }
43
43
 
44
44
  /**
45
- * The session's store, as one typed slot inside `ctx.state`.
45
+ * A pristine deep copy of the seed store.
46
46
  *
47
- * Every mutating tool goes through `retailSlot.update` (via `retailTool`),
48
- * which serializes per session: the LLM loop can run a step's tool calls
49
- * concurrently, and two interleaving async mutators would each observe the
50
- * other's half-applied changes.
47
+ * Exported so a caller needs no cast of its own. `shared.test.ts` had written
48
+ * `structuredClone(seedJson) as unknown as Store` twice a second and third
49
+ * copy of the narrowing this module already owns, each of which would keep
50
+ * compiling if `Store` grew a field the JSON does not carry. The one cast is
51
+ * `SEED`'s above, and `seed.test.ts` is what makes it honest.
52
+ */
53
+ export function seedStore(): Store {
54
+ return structuredClone(SEED);
55
+ }
56
+
57
+ /**
58
+ * The session's store, as one typed slot.
59
+ *
60
+ * Every tool goes through `retailSlot.updateTool` (via `retailTool`), so every
61
+ * tool body is handed a mutable draft of the whole store and mutates it in
62
+ * place: the ~106 KB of catalogue here is exactly the state a functional replace
63
+ * would make unwritable without hand-built spread chains three levels deep.
64
+ * What makes that safe is that the window is SYNCHRONOUS — the LLM loop runs a
65
+ * step's tool calls concurrently, and a draft that cannot span an await cannot
66
+ * interleave with another one.
51
67
  *
52
68
  * No `after` hook — unlike dispatch-center, this store has no derived field to
53
69
  * recalculate, and its one growth cap (`activity`) is held on append by
@@ -55,8 +71,6 @@ export function createDefaultState(): RetailState {
55
71
  */
56
72
  export const retailSlot = sessionSlot("retail", createDefaultState);
57
73
 
58
- export type StateSlot = SlotStateOf<typeof retailSlot>;
59
-
60
74
  export function setFocus(
61
75
  state: RetailState,
62
76
  focus: { orderId?: string; productId?: string },
@@ -157,7 +171,34 @@ interface RetailToolSpec<S extends z.ZodType<Record<string, unknown>>, R> {
157
171
  * out; everything else touches customer data. */
158
172
  requiresAuth?: boolean;
159
173
  summary: (args: z.output<S>, result: R) => string;
160
- execute: (args: z.output<S>, ctx: ToolContext) => R | Promise<R>;
174
+ /**
175
+ * Handed the store as its second argument, and SYNCHRONOUS.
176
+ *
177
+ * **Declare it BEFORE `summary` in the object literal.** TS infers this
178
+ * wrapper's generic `R` from `execute`'s return type and processes an object
179
+ * literal's properties in SOURCE ORDER, so with `summary` written first its
180
+ * `result` parameter has nothing to infer from and silently falls back to
181
+ * `unknown` — every `isToolFailure(result) ? … : result.order_id` in the
182
+ * fifteen tool files then stops compiling, or worse, stops meaning anything.
183
+ * It lives here rather than in each tool file because it is a property of
184
+ * this type: the same four lines were pasted into eight of the fifteen and
185
+ * pointed at from five more, which is a rule maintained in fourteen places.
186
+ *
187
+ * ---
188
+ *
189
+ * **The draft is passed in rather than re-read**, which is the one change the
190
+ * durable store forced on this template. The body used to open with
191
+ * `retailSlot.get(ctx)`; under the draft write model that returns the value as
192
+ * it was BEFORE this call, frozen, so a body that mutated it would be writing
193
+ * to something nothing was going to store. Passing it removes the possibility
194
+ * rather than documenting it.
195
+ *
196
+ * Synchronous for the same reason: the window cannot span an await — see
197
+ * {@link SessionSlot.update}. Every tool here is a pure function of the store,
198
+ * so none of them wants to; a tool that DID would await outside the wrapper and
199
+ * call `retailSlot.update` itself, the way `plan-and-execute`'s do.
200
+ */
201
+ execute: (args: z.output<S>, state: RetailState, ctx: ToolContext) => R;
161
202
  }
162
203
 
163
204
  function record(state: RetailState, name: string, summary: string): void {
@@ -185,27 +226,25 @@ export function retailTool<S extends z.ZodType<Record<string, unknown>>, R>(
185
226
  spec: RetailToolSpec<S, R>,
186
227
  ) {
187
228
  const requiresAuth = spec.requiresAuth ?? true;
188
- return tool({
229
+ // `updateTool` rather than `tool` + a hand-written `retailSlot.update`: it runs
230
+ // the body inside the slot's mutation window and hands it the draft, so this
231
+ // wrapper is left with only what is specific to THIS agent.
232
+ return retailSlot.updateTool({
189
233
  description: spec.description,
190
234
  inputSchema: spec.inputSchema,
191
- // `retailSlot.update` is the only caller of the serialized region in this
192
- // template, which is what keeps `update`'s non-reentrancy unreachable: a
193
- // tool body cannot nest another `update` on this slot, because tool bodies
194
- // are `spec.execute` and always run INSIDE this one.
195
- execute: (args, ctx) =>
196
- retailSlot.update(ctx, async (state) => {
197
- const typedArgs = args as z.output<S>;
198
- if (requiresAuth && !state.authenticatedUserId) {
199
- record(state, spec.name, "blocked: not authenticated");
200
- return { error: NOT_AUTHENTICATED };
201
- }
202
- const result = await spec.execute(typedArgs, ctx);
203
- record(
204
- state,
205
- spec.name,
206
- isToolFailure(result) ? `error: ${result.error}` : spec.summary(typedArgs, result),
207
- );
208
- return result;
209
- }),
235
+ execute: (args, state, ctx) => {
236
+ const typedArgs = args as z.output<S>;
237
+ if (requiresAuth && !state.authenticatedUserId) {
238
+ record(state, spec.name, "blocked: not authenticated");
239
+ return toolFailure(NOT_AUTHENTICATED);
240
+ }
241
+ const result = spec.execute(typedArgs, state, ctx);
242
+ record(
243
+ state,
244
+ spec.name,
245
+ isToolFailure(result) ? `error: ${result.error}` : spec.summary(typedArgs, result),
246
+ );
247
+ return result;
248
+ },
210
249
  });
211
250
  }