@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.
- package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- 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
|
|
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
|
|
29
|
-
// not see each other's cancellations.
|
|
30
|
-
// the slot
|
|
31
|
-
//
|
|
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 {
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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:
|
|
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((
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
23
|
-
return createToolContext
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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,
|
|
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:
|
|
41
|
+
return { ...emptyRetailState(), store: seedStore() };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* A pristine deep copy of the seed store.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
}
|