@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,23 +1,6 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
- import type { StateSlot } from "./shared.ts";
3
2
  import { storeView } from "./shared.ts";
4
- import { createDefaultState } from "./store.ts";
5
- import systemPrompt from "./system-prompt.md?raw";
6
- import { cancelPendingOrder } from "./tools/cancel_pending_order.ts";
7
- import { exchangeDeliveredOrderItems } from "./tools/exchange_delivered_order_items.ts";
8
- import { findUserIdByEmail } from "./tools/find_user_id_by_email.ts";
9
- import { findUserIdByNameZip } from "./tools/find_user_id_by_name_zip.ts";
10
- import { getItemDetails } from "./tools/get_item_details.ts";
11
- import { getOrderDetails } from "./tools/get_order_details.ts";
12
- import { getProductDetails } from "./tools/get_product_details.ts";
13
- import { getUserDetails } from "./tools/get_user_details.ts";
14
- import { listAllProductTypes } from "./tools/list_all_product_types.ts";
15
- import { modifyPendingOrderAddress } from "./tools/modify_pending_order_address.ts";
16
- import { modifyPendingOrderItems } from "./tools/modify_pending_order_items.ts";
17
- import { modifyPendingOrderPayment } from "./tools/modify_pending_order_payment.ts";
18
- import { modifyUserAddress } from "./tools/modify_user_address.ts";
19
- import { returnDeliveredOrderItems } from "./tools/return_delivered_order_items.ts";
20
- import { transferToHumanAgents } from "./tools/transfer_to_human_agents.ts";
3
+ import { retailSlot } from "./store.ts";
21
4
 
22
5
  export default agent({
23
6
  name: "Retail Support",
@@ -26,44 +9,21 @@ export default agent({
26
9
  // filled from the all-AssemblyAI pipeline at parse time. Only `stt` is
27
10
  // overridden below; `llm` and `tts` take the defaults.
28
11
 
29
- // The store lives in ctx.state, one pristine copy per session — callers must
30
- // not see each other's cancellations. Wrapped in the `StateSlot` shape
31
- // `store.ts`'s `getState`/`shared.ts`'s `storeView` both key off `agent()`
32
- // infers its session-state type parameter from BOTH `state` and `syncState`,
33
- // and `StateSlot`'s `retail` field is optional (a "weak type"), so handing
34
- // it a bare `RetailState` with no properties in common fails that inference
35
- // rather than an ordinary structural check.
36
- state: (): StateSlot => ({ retail: createDefaultState() }),
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.
37
16
 
38
17
  // One projection pushed after every tool call. It is a projection, not a
39
18
  // flag, because the state holds all six seeded customers and only the
40
19
  // authenticated one may reach the browser.
41
- syncState: storeView,
20
+ syncState: retailSlot.projection(storeView),
42
21
 
43
22
  // Callers read order numbers and ten-digit item numbers in bursts with pauses
44
23
  // inside one utterance ("W seven six seven … eight oh seven two"). The default
45
24
  // pipeline's `max_turn_silence` already tolerates that; reach for
46
25
  // `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
47
- systemPrompt,
48
26
  greeting:
49
27
  "Thanks for calling. Before I can look anything up I'll need to find your account — " +
50
28
  "what's the email address on it?",
51
-
52
- tools: {
53
- cancel_pending_order: cancelPendingOrder,
54
- exchange_delivered_order_items: exchangeDeliveredOrderItems,
55
- find_user_id_by_email: findUserIdByEmail,
56
- find_user_id_by_name_zip: findUserIdByNameZip,
57
- get_item_details: getItemDetails,
58
- get_order_details: getOrderDetails,
59
- get_product_details: getProductDetails,
60
- get_user_details: getUserDetails,
61
- list_all_product_types: listAllProductTypes,
62
- modify_pending_order_address: modifyPendingOrderAddress,
63
- modify_pending_order_items: modifyPendingOrderItems,
64
- modify_pending_order_payment: modifyPendingOrderPayment,
65
- modify_user_address: modifyUserAddress,
66
- return_delivered_order_items: returnDeliveredOrderItems,
67
- transfer_to_human_agents: transferToHumanAgents,
68
- },
69
29
  });
@@ -1,5 +1,5 @@
1
+ import type { ToolFailure } from "@alexkroman1/aai";
1
2
  import type { RetailState, User } from "./shared.ts";
2
- import type { ErrorResult } from "./store.ts";
3
3
 
4
4
  export interface AuthResult {
5
5
  user_id: string;
@@ -16,7 +16,7 @@ export interface AuthResult {
16
16
  * order you want. Re-running a finder for the SAME customer succeeds, because a
17
17
  * caller repeating their email should not hit an error.
18
18
  */
19
- export function authenticateAs(state: RetailState, user: User): AuthResult | ErrorResult {
19
+ export function authenticateAs(state: RetailState, user: User): AuthResult | ToolFailure {
20
20
  const current = state.authenticatedUserId;
21
21
  if (current && current !== user.user_id) {
22
22
  return {
@@ -1,8 +1,13 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { ChatMessage } from "@alexkroman1/aai-ui";
3
- import { 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
- import { useEffect, useRef } from "react";
6
11
  import type {
7
12
  OrderStatus,
8
13
  OrderView,
@@ -10,7 +15,7 @@ import type {
10
15
  StoreView,
11
16
  SwapOptionView,
12
17
  } from "./shared.ts";
13
- import { DEMO_PERSONAS, storeView } from "./shared.ts";
18
+ import { DEMO_PERSONAS, emptyRetailState, storeView } from "./shared.ts";
14
19
 
15
20
  const CSS = `
16
21
  @keyframes rt-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@@ -38,15 +43,30 @@ const statusColors: Record<string, string> = {
38
43
  } satisfies Record<OrderStatus, string>;
39
44
 
40
45
  // The sidebar before the first tool call, derived from the projection itself so
41
- // a new StoreView field can't miss the pre-first-call render.
42
- const EMPTY_VIEW: StoreView = storeView({});
46
+ // a new StoreView field can't miss the pre-first-call render. Built from
47
+ // `emptyRetailState` rather than `retailSlot.projection(...)(undefined)`,
48
+ // because the slot's factory lives in `store.ts` and pulls the 107 KB seed —
49
+ // importing it here would ship the whole catalog to the browser.
50
+ const EMPTY_VIEW: StoreView = storeView(emptyRetailState());
43
51
 
44
- function stateColor(state: string): string {
45
- if (state === "listening" || state === "ready") return "#16a34a";
46
- if (state === "thinking") return "#ca8a04";
47
- if (state === "speaking") return "#2563eb";
48
- return "#a1a1aa";
49
- }
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>;
50
70
 
51
71
  function Panel({ title, children }: { title: string; children: ReactNode }) {
52
72
  return (
@@ -193,13 +213,12 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
193
213
 
194
214
  function App() {
195
215
  const session = useSession();
196
- const messagesEndRef = useRef<HTMLDivElement>(null);
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();
197
220
  // The agent's own store, projected by `syncState` after every tool call.
198
- const view = useAgentState<StoreView>() ?? EMPTY_VIEW;
199
-
200
- useEffect(() => {
201
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
202
- }, [session.messages]);
221
+ const view = useAgentState<StoreView>(EMPTY_VIEW);
203
222
 
204
223
  const lastAction = view.activity.at(-1);
205
224
 
@@ -220,7 +239,7 @@ function App() {
220
239
  <span
221
240
  className="w-2 h-2 rounded-full inline-block"
222
241
  style={{
223
- background: stateColor(session.state),
242
+ background: STATE_COLORS[session.state],
224
243
  animation:
225
244
  session.state === "listening"
226
245
  ? "rt-pulse 1.5s ease-in-out infinite"
@@ -255,7 +274,10 @@ function App() {
255
274
  className="flex flex-col overflow-hidden"
256
275
  style={{ borderRight: "1px solid #e4e4e7" }}
257
276
  >
258
- <div className="rt-scroll flex-1 overflow-y-auto p-4 flex flex-col gap-2">
277
+ <AutoScroll
278
+ scrollClassName="rt-scroll overflow-y-auto"
279
+ contentClassName="p-4 flex flex-col gap-2"
280
+ >
259
281
  {session.messages.length === 0 && (
260
282
  <div className="text-center p-10 text-[13px]" style={{ color: "#a1a1aa" }}>
261
283
  Press start, then read one of the emails from “Who to be” to the agent.
@@ -277,10 +299,9 @@ function App() {
277
299
  {message.content}
278
300
  </div>
279
301
  ))}
280
- <div ref={messagesEndRef} />
281
- </div>
302
+ </AutoScroll>
282
303
 
283
- {session.userTranscript !== null && (
304
+ {speaking && (
284
305
  <div
285
306
  className="flex items-center px-4 py-2 text-xs italic min-h-8"
286
307
  style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
@@ -289,7 +310,7 @@ function App() {
289
310
  className="w-2 h-2 rounded-full inline-block mr-2"
290
311
  style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
291
312
  />
292
- {session.userTranscript === "" ? "…" : session.userTranscript}
313
+ {userTranscript}
293
314
  </div>
294
315
  )}
295
316
  {session.error && (
@@ -1,7 +1,10 @@
1
- import type { ToolContext } from "@alexkroman1/aai";
1
+ /// <reference types="vite/client" />
2
+
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";
4
- import { getState, isError } from "./store.ts";
6
+ import authoredAgent from "./agent.ts";
7
+ import { retailSlot } from "./store.ts";
5
8
 
6
9
  /** Tools that legitimately run before the caller is identified. Everything
7
10
  * else must refuse. Listed here so ADDING an unauthenticated tool is a
@@ -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
@@ -126,9 +135,9 @@ describe("the UI-update invariant", () => {
126
135
  // retailTool(): it would work, and the sidebar would sit still through it.
127
136
  test.each(registry)("%s increments callSeq and logs activity", async (name, def) => {
128
137
  const ctx = makeCtx();
129
- const before = getState(ctx).callSeq;
138
+ const before = retailSlot.get(ctx).callSeq;
130
139
  await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
131
- const state = getState(ctx);
140
+ const state = retailSlot.get(ctx);
132
141
  expect(state.callSeq, `${name} did not bump callSeq`).toBe(before + 1);
133
142
  expect(state.activity.at(-1)?.tool, `${name} logged the wrong tool name`).toBe(name);
134
143
  expect(state.activity.at(-1)?.summary).toBeTruthy();
@@ -139,7 +148,7 @@ describe("the UI-update invariant", () => {
139
148
  // disagree — the activity feed would then attribute calls to the wrong tool.
140
149
  const ctx = makeCtx();
141
150
  await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
142
- expect(getState(ctx).activity.at(-1)?.tool).toBe(name);
151
+ expect(retailSlot.get(ctx).activity.at(-1)?.tool).toBe(name);
143
152
  });
144
153
  });
145
154
 
@@ -148,8 +157,8 @@ describe("the authentication gate", () => {
148
157
  "%s refuses before the caller is identified",
149
158
  async (name, def) => {
150
159
  const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
151
- expect(isError(result), `${name} did not refuse`).toBe(true);
152
- if (!isError(result)) return;
160
+ expect(isToolFailure(result), `${name} did not refuse`).toBe(true);
161
+ if (!isToolFailure(result)) return;
153
162
  expect(result.error, `${name} refused for the wrong reason`).toContain(
154
163
  "find_user_id_by_email",
155
164
  );
@@ -162,9 +171,9 @@ describe("the authentication gate", () => {
162
171
  const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
163
172
  // A public tool may still fail on its own (deliberately bogus) arguments;
164
173
  // it must not fail on the GATE. Written as one unconditional assertion —
165
- // an `if (isError(result))` wrapper would pass vacuously for the tools
174
+ // an `if (isToolFailure(result))` wrapper would pass vacuously for the tools
166
175
  // that succeed, which is precisely the set most at risk of regressing.
167
- const blockedByGate = isError(result) && result.error.includes("find_user_id_by_email");
176
+ const blockedByGate = isToolFailure(result) && result.error.includes("find_user_id_by_email");
168
177
  expect(blockedByGate, `${name} was blocked by the auth gate`).toBe(false);
169
178
  },
170
179
  );
@@ -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,6 +1,7 @@
1
+ import { isToolFailure } from "@alexkroman1/aai";
1
2
  import { describe, expect, test } from "vitest";
2
3
  import { normalizeItemId, normalizeOrderId, resolveOrder, resolveVariantId } from "./resolve.ts";
3
- import { createDefaultState, findProduct, isError } from "./store.ts";
4
+ import { createDefaultState, findProduct } from "./store.ts";
4
5
 
5
6
  function stateFor(userId: string) {
6
7
  const state = createDefaultState();
@@ -39,13 +40,13 @@ describe("resolveOrder — canonical ids", () => {
39
40
  const state = stateFor("olivia_ito_3591");
40
41
  for (const spoken of ["#W5866402", "W 5866402", "5866402"]) {
41
42
  const order = resolveOrder(state, spoken);
42
- expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
43
+ expect.soft(isToolFailure(order) ? null : order.order_id, spoken).toBe("#W5866402");
43
44
  }
44
45
  });
45
46
 
46
47
  test("refuses another customer's order id", () => {
47
48
  const state = stateFor("olivia_ito_3591");
48
- expect(isError(resolveOrder(state, "#W4316152"))).toBe(true);
49
+ expect(isToolFailure(resolveOrder(state, "#W4316152"))).toBe(true);
49
50
  });
50
51
  });
51
52
 
@@ -54,14 +55,14 @@ describe("resolveOrder — shorthand", () => {
54
55
  const state = stateFor("olivia_ito_3591");
55
56
  for (const spoken of ["the delivered one", "my delivered order", "delivered"]) {
56
57
  const order = resolveOrder(state, spoken);
57
- expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
58
+ expect.soft(isToolFailure(order) ? null : order.order_id, spoken).toBe("#W5866402");
58
59
  }
59
60
  });
60
61
 
61
62
  test("three pending orders make bare 'my pending order' ambiguous, and it lists them", () => {
62
63
  const state = stateFor("olivia_ito_3591");
63
64
  const result = resolveOrder(state, "my pending order");
64
- if (!isError(result)) throw new Error("expected ambiguity");
65
+ if (!isToolFailure(result)) throw new Error("expected ambiguity");
65
66
  for (const id of ["#W5442520", "#W7941031", "#W3657213"]) {
66
67
  expect.soft(result.error, `ambiguity message omits ${id}`).toContain(id);
67
68
  }
@@ -71,38 +72,38 @@ describe("resolveOrder — shorthand", () => {
71
72
  const state = stateFor("olivia_ito_3591");
72
73
  const first = resolveOrder(state, "my first pending order");
73
74
  const second = resolveOrder(state, "the second pending order");
74
- expect(isError(first) ? null : first.order_id).toBe("#W5442520");
75
- expect(isError(second) ? null : second.order_id).toBe("#W7941031");
75
+ expect(isToolFailure(first) ? null : first.order_id).toBe("#W5442520");
76
+ expect(isToolFailure(second) ? null : second.order_id).toBe("#W7941031");
76
77
  });
77
78
 
78
79
  test("'last' picks the final one", () => {
79
80
  const state = stateFor("olivia_ito_3591");
80
81
  const result = resolveOrder(state, "my last pending order");
81
- expect(isError(result) ? null : result.order_id).toBe("#W3657213");
82
+ expect(isToolFailure(result) ? null : result.order_id).toBe("#W3657213");
82
83
  });
83
84
 
84
85
  test("a bare ordinal with no status word indexes all of the caller's orders", () => {
85
86
  const state = stateFor("emma_smith_8564");
86
87
  const result = resolveOrder(state, "the first one");
87
- expect(isError(result) ? null : result.order_id).toBe("#W2417020");
88
+ expect(isToolFailure(result) ? null : result.order_id).toBe("#W2417020");
88
89
  });
89
90
 
90
91
  test("a status the caller has none of says what they do have", () => {
91
92
  const state = stateFor("harper_brown_7363");
92
93
  const result = resolveOrder(state, "my cancelled order");
93
- if (!isError(result)) throw new Error("expected refusal");
94
+ if (!isToolFailure(result)) throw new Error("expected refusal");
94
95
  expect(result.error).toContain("delivered");
95
96
  expect(result.error).toContain("pending");
96
97
  });
97
98
 
98
99
  test("an out-of-range ordinal is refused, not clamped", () => {
99
100
  const state = stateFor("emma_smith_8564");
100
- expect(isError(resolveOrder(state, "the fourth pending order"))).toBe(true);
101
+ expect(isToolFailure(resolveOrder(state, "the fourth pending order"))).toBe(true);
101
102
  });
102
103
 
103
104
  test("unresolvable input is refused before authentication too", () => {
104
105
  const state = createDefaultState();
105
- expect(isError(resolveOrder(state, "my pending order"))).toBe(true);
106
+ expect(isToolFailure(resolveOrder(state, "my pending order"))).toBe(true);
106
107
  });
107
108
  });
108
109
 
@@ -110,7 +111,7 @@ describe("resolveVariantId", () => {
110
111
  const state = createDefaultState();
111
112
  function teaKettle() {
112
113
  const product = findProduct(state, "9832717871");
113
- if (isError(product)) throw new Error(product.error);
114
+ if (isToolFailure(product)) throw new Error(product.error);
114
115
  return product;
115
116
  }
116
117
 
@@ -120,7 +121,7 @@ describe("resolveVariantId", () => {
120
121
  });
121
122
 
122
123
  test("refuses an item id belonging to a different product", () => {
123
- expect(isError(resolveVariantId(teaKettle(), "4725166838"))).toBe(true);
124
+ expect(isToolFailure(resolveVariantId(teaKettle(), "4725166838"))).toBe(true);
124
125
  });
125
126
 
126
127
  test("matches a spoken option phrase", () => {
@@ -135,13 +136,13 @@ describe("resolveVariantId", () => {
135
136
 
136
137
  test("an ambiguous phrase lists the candidates with their options", () => {
137
138
  const result = resolveVariantId(teaKettle(), "glass");
138
- if (!isError(result)) throw new Error("expected ambiguity");
139
+ if (!isToolFailure(result)) throw new Error("expected ambiguity");
139
140
  expect(result.error).toContain("glass");
140
141
  expect(result.error.match(/\d{10}/g)?.length).toBeGreaterThan(1);
141
142
  });
142
143
 
143
144
  test("a phrase matching nothing is refused", () => {
144
- expect(isError(resolveVariantId(teaKettle(), "titanium"))).toBe(true);
145
+ expect(isToolFailure(resolveVariantId(teaKettle(), "titanium"))).toBe(true);
145
146
  });
146
147
 
147
148
  test("availableOnly excludes unavailable variants from matching", () => {
@@ -1,5 +1,17 @@
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";
1
13
  import type { Order, OrderStatus, Product, RetailState } from "./shared.ts";
2
- import { authenticatedUser, type ErrorResult, isError } from "./store.ts";
14
+ import { authenticatedUser } from "./store.ts";
3
15
 
4
16
  /** `#W5866402` from anything STT plausibly produces for it. */
5
17
  export function normalizeOrderId(spoken: string): string {
@@ -9,27 +21,13 @@ export function normalizeOrderId(spoken: string): string {
9
21
 
10
22
  /** Item ids are 10 digits; callers read them in groups. */
11
23
  export function normalizeItemId(spoken: string): string {
12
- return spoken.replace(/\D/g, "");
24
+ return spokenDigits(spoken);
13
25
  }
14
26
 
15
27
  /** A digit run long enough to be an id rather than an ordinal. */
16
28
  const LOOKS_LIKE_ORDER_ID = /\d[\d\s-]{5,}/;
17
29
  const LOOKS_LIKE_ITEM_ID = /(?:\d[\s-]*){8,}/;
18
30
 
19
- const ORDINALS: Record<string, number> = {
20
- first: 0,
21
- "1st": 0,
22
- second: 1,
23
- "2nd": 1,
24
- third: 2,
25
- "3rd": 2,
26
- fourth: 3,
27
- "4th": 3,
28
- fifth: 4,
29
- "5th": 4,
30
- last: -1,
31
- };
32
-
33
31
  /** Spoken status words → the statuses they select. `pending` deliberately
34
32
  * excludes `pending (item modified)`, which no tool will accept anyway. */
35
33
  const STATUS_WORDS: [string, OrderStatus[]][] = [
@@ -53,9 +51,9 @@ function describeOrder(order: Order): string {
53
51
  * Ambiguity is always an error listing the candidates, never a guess — the
54
52
  * consequence of guessing here is cancelling the wrong order.
55
53
  */
56
- export function resolveOrder(state: RetailState, spoken: string): Order | ErrorResult {
54
+ export function resolveOrder(state: RetailState, spoken: string): Order | ToolFailure {
57
55
  const user = authenticatedUser(state);
58
- if (isError(user)) return user;
56
+ if (isToolFailure(user)) return user;
59
57
 
60
58
  const owned = user.orders
61
59
  .map((id) => state.store.orders[id])
@@ -71,6 +69,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ErrorR
71
69
  const statusEntry = STATUS_WORDS.find(([word]) => text.includes(word));
72
70
  const candidates = statusEntry ? owned.filter((o) => statusEntry[1].includes(o.status)) : owned;
73
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.
74
75
  if (candidates.length === 0) {
75
76
  const have = [...new Set(owned.map((o) => o.status))].join(", ");
76
77
  return {
@@ -78,28 +79,9 @@ export function resolveOrder(state: RetailState, spoken: string): Order | ErrorR
78
79
  };
79
80
  }
80
81
 
81
- const ordinalWord = Object.keys(ORDINALS).find((word) => new RegExp(`\\b${word}\\b`).test(text));
82
- if (ordinalWord) {
83
- const index = ORDINALS[ordinalWord] ?? 0;
84
- const picked = index === -1 ? candidates.at(-1) : candidates[index];
85
- if (!picked) {
86
- return {
87
- error: `There is no ${ordinalWord} such order — the customer has ${candidates.length}: ${candidates
88
- .map(describeOrder)
89
- .join("; ")}.`,
90
- };
91
- }
92
- return picked;
93
- }
94
-
95
- const only = candidates[0];
96
- if (candidates.length === 1 && only) return only;
97
-
98
- return {
99
- error: `That is ambiguous — ${candidates.length} orders match. Ask which one: ${candidates
100
- .map(describeOrder)
101
- .join("; ")}.`,
102
- };
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 });
103
85
  }
104
86
 
105
87
  /**
@@ -110,7 +92,7 @@ export function resolveVariantId(
110
92
  product: Product,
111
93
  spoken: string,
112
94
  opts: { availableOnly?: boolean } = {},
113
- ): string | ErrorResult {
95
+ ): string | ToolFailure {
114
96
  if (LOOKS_LIKE_ITEM_ID.test(spoken)) {
115
97
  const itemId = normalizeItemId(spoken);
116
98
  const variant = product.variants[itemId];
@@ -126,33 +108,16 @@ export function resolveVariantId(
126
108
  return itemId;
127
109
  }
128
110
 
129
- const text = spoken.toLowerCase();
130
111
  const pool = Object.values(product.variants).filter((v) => !opts.availableOnly || v.available);
131
112
 
132
- let best = 0;
133
- const scored = pool.map((variant) => {
134
- const score = Object.values(variant.options).filter((value) =>
135
- text.includes(value.toLowerCase()),
136
- ).length;
137
- best = Math.max(best, score);
138
- 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,
139
121
  });
140
-
141
- if (best === 0) {
142
- return {
143
- error: `No ${product.name} option matches "${spoken}". Options available: ${pool
144
- .map((v) => `${v.item_id} (${Object.values(v.options).join(", ")})`)
145
- .join("; ")}.`,
146
- };
147
- }
148
-
149
- const winners = scored.filter((s) => s.score === best);
150
- const winner = winners[0]?.variant;
151
- if (winners.length === 1 && winner) return winner.item_id;
152
-
153
- return {
154
- error: `"${spoken}" matches ${winners.length} ${product.name} options. Ask which: ${winners
155
- .map((s) => `${s.variant.item_id} (${Object.values(s.variant.options).join(", ")})`)
156
- .join("; ")}.`,
157
- };
122
+ return isToolFailure(picked) ? picked : picked.item_id;
158
123
  }