@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,36 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import { createToolContext, runTool, toolOf, withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
3
5
|
import { describe, expect, test } from "vitest";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
import authoredAgent from "./agent.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
10
|
+
*
|
|
11
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
12
|
+
* this file SHIPS: it is what a scaffolded project runs, so it may not import
|
|
13
|
+
* anything outside its own template, and `import.meta.glob` is expanded against
|
|
14
|
+
* the file containing it either way. This is the pattern a user writes.
|
|
15
|
+
*/
|
|
16
|
+
const agentDef = withDiscoveredTools(
|
|
17
|
+
authoredAgent,
|
|
18
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
import { calculateTotal, orderSlot, orderView, type Pizza, pizzaPrice } from "./shared.ts";
|
|
13
22
|
|
|
14
23
|
// ─── Test doubles ────────────────────────────────────────────────────────────
|
|
15
24
|
|
|
16
|
-
/** Each context
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const def = agentDef.tools[name];
|
|
27
|
-
if (!def) throw new Error(`tool ${name} not defined on agent`);
|
|
28
|
-
return def;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function run(name: string, args: Record<string, unknown>, ctx: ToolContext) {
|
|
32
|
-
return await getTool(name).execute(args, ctx);
|
|
33
|
-
}
|
|
25
|
+
/** Each context owns its OWN slot store, so two contexts are two carts. Its
|
|
26
|
+
* default `db` rejects every query, which is right here: this template keeps
|
|
27
|
+
* its cart in a session slot and must never touch storage. */
|
|
28
|
+
const makeCtx = (): ToolContext => createToolContext();
|
|
29
|
+
|
|
30
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
31
|
+
* and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
|
|
32
|
+
* what is local is only which agent they run against. */
|
|
33
|
+
const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
|
|
34
|
+
runTool(agentDef, name, args, ctx);
|
|
34
35
|
|
|
35
36
|
const margherita: Omit<Pizza, "id"> = {
|
|
36
37
|
size: "medium",
|
|
@@ -71,7 +72,7 @@ describe("pricing (shared.ts)", () => {
|
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
test("add_pizza schema defaults quantity to 1 and rejects non-positive quantities", async () => {
|
|
74
|
-
const schema =
|
|
75
|
+
const schema = toolOf(agentDef, "add_pizza").inputSchema;
|
|
75
76
|
if (!schema) throw new Error("add_pizza has no input schema");
|
|
76
77
|
// Validate through the Standard Schema contract — the vendor-neutral
|
|
77
78
|
// interface every inputSchema carries.
|
|
@@ -90,7 +91,7 @@ describe("pricing (shared.ts)", () => {
|
|
|
90
91
|
|
|
91
92
|
describe("tool flow (add → update → remove → place_order)", () => {
|
|
92
93
|
test("full ordering round-trip keeps state, totals, and IDs consistent", async () => {
|
|
93
|
-
const
|
|
94
|
+
const ctx = makeCtx();
|
|
94
95
|
|
|
95
96
|
// Empty order guards
|
|
96
97
|
expect(await run("view_order", {}, ctx)).toEqual({ message: "The order is empty." });
|
|
@@ -151,31 +152,35 @@ describe("tool flow (add → update → remove → place_order)", () => {
|
|
|
151
152
|
expect(await run("place_order", {}, ctx)).toEqual({ error: "Cannot place an empty order." });
|
|
152
153
|
});
|
|
153
154
|
|
|
154
|
-
test("
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
test("two independent contexts never share pizzas or names", async () => {
|
|
156
|
+
// What this really checks, and it is worth checking: the cart lives in the
|
|
157
|
+
// SLOT and not in a module-level variable. `createToolContext()` hands each
|
|
158
|
+
// call its own detached slot store, so passing two distinct session ids
|
|
159
|
+
// would prove nothing extra — the isolation is per store, and the store is
|
|
160
|
+
// per context. A template that cached its order in a module would fail here.
|
|
161
|
+
const firstCall = makeCtx();
|
|
162
|
+
const secondCall = makeCtx();
|
|
158
163
|
|
|
159
164
|
await run(
|
|
160
165
|
"add_pizza",
|
|
161
166
|
{ size: "large", crust: "thin", toppings: ["bacon"], quantity: 1 },
|
|
162
|
-
|
|
167
|
+
firstCall,
|
|
163
168
|
);
|
|
164
|
-
await run("set_customer_name", { name: "Alice" },
|
|
169
|
+
await run("set_customer_name", { name: "Alice" }, firstCall);
|
|
165
170
|
|
|
166
|
-
expect(await run("view_order", {},
|
|
171
|
+
expect(await run("view_order", {}, secondCall)).toEqual({ message: "The order is empty." });
|
|
167
172
|
|
|
168
|
-
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 },
|
|
169
|
-
const placedB = (await run("place_order", {},
|
|
173
|
+
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, secondCall);
|
|
174
|
+
const placedB = (await run("place_order", {}, secondCall)) as {
|
|
170
175
|
customerName: string;
|
|
171
176
|
pizzas: number;
|
|
172
177
|
};
|
|
173
|
-
//
|
|
178
|
+
// The second context never sees the first's customer name or pizzas.
|
|
174
179
|
expect(placedB.customerName).toBe("Guest");
|
|
175
180
|
expect(placedB.pizzas).toBe(1);
|
|
176
181
|
|
|
177
|
-
//
|
|
178
|
-
const viewA = (await run("view_order", {},
|
|
182
|
+
// And the first context's cart survives the second's checkout untouched.
|
|
183
|
+
const viewA = (await run("view_order", {}, firstCall)) as { pizzas: unknown[] };
|
|
179
184
|
expect(viewA.pizzas).toHaveLength(1);
|
|
180
185
|
});
|
|
181
186
|
});
|
|
@@ -188,14 +193,14 @@ describe("tool flow (add → update → remove → place_order)", () => {
|
|
|
188
193
|
|
|
189
194
|
describe("orderView projection", () => {
|
|
190
195
|
test("reflects the live cart", async () => {
|
|
191
|
-
const
|
|
196
|
+
const ctx = makeCtx();
|
|
192
197
|
await run(
|
|
193
198
|
"add_pizza",
|
|
194
199
|
{ size: "large", crust: "stuffed", toppings: ["pepperoni", "extra_cheese"], quantity: 2 },
|
|
195
200
|
ctx,
|
|
196
201
|
);
|
|
197
202
|
|
|
198
|
-
const view = orderView(orderSlot.
|
|
203
|
+
const view = orderView(orderSlot.get(ctx));
|
|
199
204
|
expect(view.orderPlaced).toBe(false);
|
|
200
205
|
const item = view.pizzas[0];
|
|
201
206
|
if (!item) throw new Error("no pizza in the projection");
|
|
@@ -213,11 +218,11 @@ describe("orderView projection", () => {
|
|
|
213
218
|
test("survives checkout, which clears the cart but keeps the confirmation", async () => {
|
|
214
219
|
// The reason `placed` lives in state at all: the cart is emptied on
|
|
215
220
|
// checkout, and the UI still has to show the order that was just placed.
|
|
216
|
-
const
|
|
221
|
+
const ctx = makeCtx();
|
|
217
222
|
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, ctx);
|
|
218
223
|
await run("place_order", {}, ctx);
|
|
219
224
|
|
|
220
|
-
const view = orderView(orderSlot.
|
|
225
|
+
const view = orderView(orderSlot.get(ctx));
|
|
221
226
|
expect(view.orderPlaced).toBe(true);
|
|
222
227
|
expect(view.pizzas).toEqual([]);
|
|
223
228
|
expect(view.estimatedMinutes).toBe(20);
|
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
import { agent
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
CRUSTS,
|
|
5
|
-
calculateTotal,
|
|
6
|
-
formatPrice,
|
|
7
|
-
menuText,
|
|
8
|
-
orderSlot,
|
|
9
|
-
orderView,
|
|
10
|
-
type Pizza,
|
|
11
|
-
resetOrder,
|
|
12
|
-
SIZES,
|
|
13
|
-
} from "./shared.ts";
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
2
|
+
import { menuText, orderSlot, orderView } from "./shared.ts";
|
|
14
3
|
import systemPrompt from "./system-prompt.md?raw";
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
// construction, so concurrent customers each get their own cart.
|
|
5
|
+
// The in-progress order lives in one `sessionSlot` (see shared.ts) —
|
|
6
|
+
// per-session by construction, so concurrent customers each get their own cart.
|
|
7
|
+
// The six tools that read and write it are the six files in `tools/`: a file
|
|
8
|
+
// there IS a tool, named by its own filename, so nothing registers them here.
|
|
21
9
|
|
|
22
10
|
export default agent({
|
|
23
11
|
name: "Pizza Palace",
|
|
12
|
+
// `orderSlot.state` IS the `() => ({ [slot.key]: slot.create() })` factory,
|
|
13
|
+
// so the session's cart exists before the first tool call — which is what a
|
|
14
|
+
// resumed connection needs to have something to project.
|
|
24
15
|
// The cart, pushed to the client after every tool call. Replaces a
|
|
25
16
|
// `ctx.send("order", ...)` in each of the five order tools, and the
|
|
26
17
|
// event-diffing the client had to do to rebuild the cart from them.
|
|
@@ -30,144 +21,4 @@ export default agent({
|
|
|
30
21
|
systemPrompt: `${systemPrompt}\n${menuText()}`,
|
|
31
22
|
greeting:
|
|
32
23
|
"Welcome to Pizza Palace. I can help you build your perfect pizza. What would you like to order?",
|
|
33
|
-
|
|
34
|
-
tools: {
|
|
35
|
-
add_pizza: tool({
|
|
36
|
-
description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
|
|
37
|
-
inputSchema: z.object({
|
|
38
|
-
size: sizes,
|
|
39
|
-
crust: crusts,
|
|
40
|
-
toppings: z
|
|
41
|
-
.array(z.string())
|
|
42
|
-
.describe("List of topping names, e.g. ['pepperoni', 'mushrooms']"),
|
|
43
|
-
quantity: z.number().int().min(1).default(1),
|
|
44
|
-
}),
|
|
45
|
-
async execute(args, ctx) {
|
|
46
|
-
const order = orderSlot.get(ctx);
|
|
47
|
-
|
|
48
|
-
const pizza: Pizza = {
|
|
49
|
-
id: order.nextId,
|
|
50
|
-
size: args.size,
|
|
51
|
-
crust: args.crust,
|
|
52
|
-
toppings: args.toppings,
|
|
53
|
-
quantity: args.quantity,
|
|
54
|
-
};
|
|
55
|
-
order.pizzas.push(pizza);
|
|
56
|
-
order.nextId++;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
added: pizza,
|
|
60
|
-
orderTotal: formatPrice(calculateTotal(order.pizzas)),
|
|
61
|
-
itemCount: order.pizzas.length,
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
}),
|
|
65
|
-
|
|
66
|
-
place_order: tool({
|
|
67
|
-
description:
|
|
68
|
-
"Place the final order. Use when the customer confirms they are done and ready to order.",
|
|
69
|
-
async execute(_args, ctx) {
|
|
70
|
-
const order = orderSlot.get(ctx);
|
|
71
|
-
const pizzas = order.pizzas;
|
|
72
|
-
if (pizzas.length === 0) return { error: "Cannot place an empty order." };
|
|
73
|
-
|
|
74
|
-
const customerName = order.customerName ?? "Guest";
|
|
75
|
-
const total = formatPrice(calculateTotal(pizzas));
|
|
76
|
-
const orderNumber = Math.floor(1000 + Math.random() * 9000);
|
|
77
|
-
|
|
78
|
-
const estimatedMinutes = 15 + pizzas.length * 5;
|
|
79
|
-
// The order is submitted — clear the cart so a follow-up order starts
|
|
80
|
-
// fresh, but keep the confirmation in state so the UI can show it.
|
|
81
|
-
resetOrder(ctx, { orderNumber, total, estimatedMinutes });
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
orderNumber,
|
|
85
|
-
customerName,
|
|
86
|
-
pizzas: pizzas.length,
|
|
87
|
-
total,
|
|
88
|
-
estimatedMinutes,
|
|
89
|
-
};
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
|
|
93
|
-
remove_pizza: tool({
|
|
94
|
-
description: "Remove a pizza from the order by its ID.",
|
|
95
|
-
inputSchema: z.object({
|
|
96
|
-
pizza_id: z.number().describe("The pizza ID to remove"),
|
|
97
|
-
}),
|
|
98
|
-
async execute(args, ctx) {
|
|
99
|
-
const order = orderSlot.get(ctx);
|
|
100
|
-
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
101
|
-
if (idx === -1) return { error: "Pizza not found in the order." };
|
|
102
|
-
|
|
103
|
-
const [removed] = order.pizzas.splice(idx, 1);
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
removed,
|
|
107
|
-
orderTotal: formatPrice(calculateTotal(order.pizzas)),
|
|
108
|
-
itemCount: order.pizzas.length,
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
}),
|
|
112
|
-
|
|
113
|
-
set_customer_name: tool({
|
|
114
|
-
description: "Set the customer name for the order.",
|
|
115
|
-
inputSchema: z.object({
|
|
116
|
-
name: z.string(),
|
|
117
|
-
}),
|
|
118
|
-
async execute(args, ctx) {
|
|
119
|
-
orderSlot.get(ctx).customerName = args.name;
|
|
120
|
-
return { name: args.name };
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
|
|
124
|
-
update_pizza: tool({
|
|
125
|
-
description: "Update an existing pizza in the order. Only provided fields are changed.",
|
|
126
|
-
inputSchema: z.object({
|
|
127
|
-
pizza_id: z.number(),
|
|
128
|
-
size: sizes.optional(),
|
|
129
|
-
crust: crusts.optional(),
|
|
130
|
-
toppings: z.array(z.string()).optional(),
|
|
131
|
-
quantity: z.number().int().min(1).optional(),
|
|
132
|
-
}),
|
|
133
|
-
async execute(args, ctx) {
|
|
134
|
-
const order = orderSlot.get(ctx);
|
|
135
|
-
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
136
|
-
if (idx === -1) return { error: "Pizza not found in the order." };
|
|
137
|
-
|
|
138
|
-
const pizza: Pizza = { ...order.pizzas[idx]! };
|
|
139
|
-
if (args.size !== undefined) pizza.size = args.size;
|
|
140
|
-
if (args.crust !== undefined) pizza.crust = args.crust;
|
|
141
|
-
if (args.toppings !== undefined) pizza.toppings = args.toppings;
|
|
142
|
-
if (args.quantity !== undefined) pizza.quantity = args.quantity;
|
|
143
|
-
|
|
144
|
-
order.pizzas[idx] = pizza;
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
updated: pizza,
|
|
148
|
-
orderTotal: formatPrice(calculateTotal(order.pizzas)),
|
|
149
|
-
};
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
|
|
153
|
-
view_order: tool({
|
|
154
|
-
description: "View the current order summary with all pizzas and total price.",
|
|
155
|
-
async execute(_args, ctx) {
|
|
156
|
-
const pizzas = orderSlot.get(ctx).pizzas;
|
|
157
|
-
if (pizzas.length === 0) return { message: "The order is empty." };
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
pizzas: pizzas.map((p) => ({
|
|
161
|
-
id: p.id,
|
|
162
|
-
description: `${p.quantity}x ${p.size} ${p.crust} crust with ${p.toppings.length > 0 ? p.toppings.join(", ") : "cheese only"}`,
|
|
163
|
-
size: p.size,
|
|
164
|
-
crust: p.crust,
|
|
165
|
-
toppings: p.toppings,
|
|
166
|
-
quantity: p.quantity,
|
|
167
|
-
})),
|
|
168
|
-
orderTotal: formatPrice(calculateTotal(pizzas)),
|
|
169
|
-
};
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
},
|
|
173
24
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type DeepReadonly, sessionSlot } from "@alexkroman1/aai";
|
|
2
2
|
|
|
3
3
|
export const SIZES = ["small", "medium", "large"] as const;
|
|
4
4
|
export const CRUSTS = ["thin", "regular", "thick", "stuffed"] as const;
|
|
@@ -33,7 +33,7 @@ export const MENU = {
|
|
|
33
33
|
},
|
|
34
34
|
} as const;
|
|
35
35
|
|
|
36
|
-
export function calculateTotal(pizzas:
|
|
36
|
+
export function calculateTotal(pizzas: readonly ReadonlyPizza[]): number {
|
|
37
37
|
return pizzas.reduce((total, pizza) => total + pizzaPrice(pizza), 0);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -62,7 +62,7 @@ export function menuText(): string {
|
|
|
62
62
|
].join("\n");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export function pizzaPrice(p:
|
|
65
|
+
export function pizzaPrice(p: ReadonlyPizza): number {
|
|
66
66
|
const base = MENU.sizes[p.size];
|
|
67
67
|
const crust = MENU.crusts[p.crust];
|
|
68
68
|
const toppings = p.toppings.reduce(
|
|
@@ -73,9 +73,9 @@ export function pizzaPrice(p: Pizza): number {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// ── Order state ──────────────────────────────────────────────────────────────
|
|
76
|
-
// The in-progress order is session-scoped scratch, so it lives in
|
|
77
|
-
//
|
|
78
|
-
//
|
|
76
|
+
// The in-progress order is session-scoped scratch, so it lives in one
|
|
77
|
+
// `sessionSlot` keyed per session — concurrent customers each get their own
|
|
78
|
+
// cart.
|
|
79
79
|
|
|
80
80
|
export interface OrderState {
|
|
81
81
|
pizzas: Pizza[];
|
|
@@ -93,19 +93,40 @@ export function emptyOrder(): OrderState {
|
|
|
93
93
|
return { pizzas: [], nextId: 1, customerName: null };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** The session's cart, as one typed slot
|
|
96
|
+
/** The session's cart, as one typed slot. */
|
|
97
97
|
export const orderSlot = sessionSlot("order", emptyOrder);
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* A pizza as a READ hands it out, and the cart likewise: deep-frozen, and typed
|
|
101
|
+
* to say so.
|
|
102
|
+
*
|
|
103
|
+
* `pizzaPrice` and `calculateTotal` take the readonly shape because they are
|
|
104
|
+
* pure and are called from both halves of the slot — a mutable `Pizza` still
|
|
105
|
+
* satisfies it, so an `updateTool` draft passes unchanged, while a helper that
|
|
106
|
+
* WOULD have mutated stops compiling instead of throwing on its first call.
|
|
107
|
+
*/
|
|
108
|
+
export type ReadonlyPizza = DeepReadonly<Pizza>;
|
|
109
|
+
export type FrozenOrderState = DeepReadonly<OrderState>;
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
112
|
* Clear the cart after checkout so a follow-up order starts fresh. The
|
|
103
113
|
* `placed` confirmation is passed through, because the UI is still showing
|
|
104
114
|
* it — clearing the pizzas is what "reset" means here, not forgetting the
|
|
105
115
|
* order that was just submitted.
|
|
116
|
+
*
|
|
117
|
+
* It takes the DRAFT, not the context, and that is the shape to copy. It used to
|
|
118
|
+
* call `orderSlot.set(ctx, …)` — which is a second write to the same slot from
|
|
119
|
+
* inside a mutating tool body, so the draft stored on the way out overwrote it
|
|
120
|
+
* and the cart was never cleared. `slot.set` refuses that outright now; a helper
|
|
121
|
+
* called from inside a mutation mutates what it was handed.
|
|
106
122
|
*/
|
|
107
|
-
export function resetOrder(
|
|
108
|
-
|
|
123
|
+
export function resetOrder(order: OrderState, placed?: OrderState["placed"]): void {
|
|
124
|
+
const fresh = emptyOrder();
|
|
125
|
+
order.pizzas = fresh.pizzas;
|
|
126
|
+
order.nextId = fresh.nextId;
|
|
127
|
+
order.customerName = fresh.customerName;
|
|
128
|
+
if (placed) order.placed = placed;
|
|
129
|
+
else delete order.placed;
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
/**
|
|
@@ -114,7 +135,9 @@ export function resetOrder(ctx: ToolContext, placed?: OrderState["placed"]): voi
|
|
|
114
135
|
* leaves the server.
|
|
115
136
|
*/
|
|
116
137
|
export interface OrderView {
|
|
117
|
-
|
|
138
|
+
/** Readonly, because the projection hands the client the slot's own list
|
|
139
|
+
* rather than a copy — see {@link ReadonlyPizza}. */
|
|
140
|
+
pizzas: readonly ReadonlyPizza[];
|
|
118
141
|
total: string;
|
|
119
142
|
orderPlaced: boolean;
|
|
120
143
|
orderNumber?: number;
|
|
@@ -123,7 +146,7 @@ export interface OrderView {
|
|
|
123
146
|
|
|
124
147
|
/** Takes the cart itself, not the slot — `orderSlot.projection` supplies a real
|
|
125
148
|
* one even before the first tool call, so there is nothing to optional-chain. */
|
|
126
|
-
export function orderView(order:
|
|
149
|
+
export function orderView(order: FrozenOrderState): OrderView {
|
|
127
150
|
const placed = order.placed;
|
|
128
151
|
return {
|
|
129
152
|
pizzas: order.pizzas,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
3
|
+
|
|
4
|
+
export default orderSlot.updateTool({
|
|
5
|
+
description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
size: z.enum(SIZES),
|
|
8
|
+
crust: z.enum(CRUSTS),
|
|
9
|
+
toppings: z
|
|
10
|
+
.array(z.string())
|
|
11
|
+
.describe("List of topping names, e.g. ['pepperoni', 'mushrooms']"),
|
|
12
|
+
quantity: z.number().int().min(1).default(1),
|
|
13
|
+
}),
|
|
14
|
+
execute(args, order) {
|
|
15
|
+
const pizza: Pizza = {
|
|
16
|
+
id: order.nextId,
|
|
17
|
+
size: args.size,
|
|
18
|
+
crust: args.crust,
|
|
19
|
+
toppings: args.toppings,
|
|
20
|
+
quantity: args.quantity,
|
|
21
|
+
};
|
|
22
|
+
order.pizzas.push(pizza);
|
|
23
|
+
order.nextId++;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
added: pizza,
|
|
27
|
+
orderTotal: formatPrice(calculateTotal(order.pizzas)),
|
|
28
|
+
itemCount: order.pizzas.length,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { calculateTotal, formatPrice, orderSlot, resetOrder } from "../shared.ts";
|
|
3
|
+
|
|
4
|
+
export default orderSlot.updateTool({
|
|
5
|
+
description:
|
|
6
|
+
"Place the final order. Use when the customer confirms they are done and ready to order.",
|
|
7
|
+
execute(_args, order) {
|
|
8
|
+
const pizzas = order.pizzas;
|
|
9
|
+
if (pizzas.length === 0) return toolFailure("Cannot place an empty order.");
|
|
10
|
+
|
|
11
|
+
const customerName = order.customerName ?? "Guest";
|
|
12
|
+
const total = formatPrice(calculateTotal(pizzas));
|
|
13
|
+
const orderNumber = Math.floor(1000 + Math.random() * 9000);
|
|
14
|
+
|
|
15
|
+
const estimatedMinutes = 15 + pizzas.length * 5;
|
|
16
|
+
// The order is submitted — clear the cart so a follow-up order starts
|
|
17
|
+
// fresh, but keep the confirmation in state so the UI can show it.
|
|
18
|
+
resetOrder(order, { orderNumber, total, estimatedMinutes });
|
|
19
|
+
|
|
20
|
+
return { orderNumber, customerName, pizzas: pizzas.length, total, estimatedMinutes };
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
|
|
4
|
+
|
|
5
|
+
export default orderSlot.updateTool({
|
|
6
|
+
description: "Remove a pizza from the order by its ID.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
pizza_id: z.number().describe("The pizza ID to remove"),
|
|
9
|
+
}),
|
|
10
|
+
execute(args, order) {
|
|
11
|
+
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
12
|
+
if (idx === -1) return toolFailure("Pizza not found in the order.");
|
|
13
|
+
|
|
14
|
+
const [removed] = order.pizzas.splice(idx, 1);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
removed,
|
|
18
|
+
orderTotal: formatPrice(calculateTotal(order.pizzas)),
|
|
19
|
+
itemCount: order.pizzas.length,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { orderSlot } from "../shared.ts";
|
|
3
|
+
|
|
4
|
+
export default orderSlot.updateTool({
|
|
5
|
+
description: "Set the customer name for the order.",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
}),
|
|
9
|
+
execute(args, order) {
|
|
10
|
+
order.customerName = args.name;
|
|
11
|
+
return { name: args.name };
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CRUSTS, calculateTotal, formatPrice, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
4
|
+
|
|
5
|
+
export default orderSlot.updateTool({
|
|
6
|
+
description: "Update an existing pizza in the order. Only provided fields are changed.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
pizza_id: z.number(),
|
|
9
|
+
size: z.enum(SIZES).optional(),
|
|
10
|
+
crust: z.enum(CRUSTS).optional(),
|
|
11
|
+
toppings: z.array(z.string()).optional(),
|
|
12
|
+
quantity: z.number().int().min(1).optional(),
|
|
13
|
+
}),
|
|
14
|
+
execute(args, order) {
|
|
15
|
+
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
16
|
+
if (idx === -1) return toolFailure("Pizza not found in the order.");
|
|
17
|
+
|
|
18
|
+
const pizza: Pizza = { ...order.pizzas[idx]! };
|
|
19
|
+
if (args.size !== undefined) pizza.size = args.size;
|
|
20
|
+
if (args.crust !== undefined) pizza.crust = args.crust;
|
|
21
|
+
if (args.toppings !== undefined) pizza.toppings = args.toppings;
|
|
22
|
+
if (args.quantity !== undefined) pizza.quantity = args.quantity;
|
|
23
|
+
|
|
24
|
+
order.pizzas[idx] = pizza;
|
|
25
|
+
|
|
26
|
+
return { updated: pizza, orderTotal: formatPrice(calculateTotal(order.pizzas)) };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { calculateTotal, formatPrice, orderSlot } from "../shared.ts";
|
|
2
|
+
|
|
3
|
+
export default orderSlot.tool({
|
|
4
|
+
description: "View the current order summary with all pizzas and total price.",
|
|
5
|
+
execute(_args, order) {
|
|
6
|
+
const pizzas = order.pizzas;
|
|
7
|
+
if (pizzas.length === 0) return { message: "The order is empty." };
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
pizzas: pizzas.map((p) => ({
|
|
11
|
+
id: p.id,
|
|
12
|
+
description: `${p.quantity}x ${p.size} ${p.crust} crust with ${p.toppings.length > 0 ? p.toppings.join(", ") : "cheese only"}`,
|
|
13
|
+
size: p.size,
|
|
14
|
+
crust: p.crust,
|
|
15
|
+
toppings: p.toppings,
|
|
16
|
+
quantity: p.quantity,
|
|
17
|
+
})),
|
|
18
|
+
orderTotal: formatPrice(calculateTotal(pizzas)),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|