@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.
- 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 +32 -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 +21 -30
- package/dist/templates/dispatch-center/agent.ts +4 -31
- package/dist/templates/dispatch-center/client.tsx +41 -28
- package/dist/templates/dispatch-center/shared.ts +73 -66
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
- package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
- package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
- package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
- 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 +32 -28
- package/dist/templates/infocom-adventure/shared.ts +36 -16
- 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 +59 -58
- package/dist/templates/pizza-ordering/agent.ts +10 -159
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +44 -27
- 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 +319 -256
- package/dist/templates/retail/agent.ts +6 -46
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +45 -24
- package/dist/templates/retail/registry.test.ts +40 -25
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +32 -67
- package/dist/templates/retail/shared.test.ts +21 -19
- package/dist/templates/retail/shared.ts +61 -23
- package/dist/templates/retail/store.test.ts +47 -52
- package/dist/templates/retail/store.ts +92 -79
- package/dist/templates/retail/swap.test.ts +20 -17
- package/dist/templates/retail/swap.ts +7 -14
- package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
- 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 +7 -8
- package/dist/templates/retail/tools/get_order_details.ts +7 -8
- package/dist/templates/retail/tools/get_product_details.ts +6 -8
- package/dist/templates/retail/tools/get_user_details.ts +6 -8
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
- package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
- package/dist/templates/retail/tools/modify_user_address.ts +6 -10
- package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +169 -54
- package/dist/templates/solo-rpg/agent.ts +4 -22
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +20 -20
- package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
- package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
- package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
- package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
- package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
- package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
- 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,45 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import { createToolContext, runTool, toolOf, withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
2
5
|
import { describe, expect, test } from "vitest";
|
|
3
|
-
import
|
|
4
|
-
|
|
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";
|
|
5
22
|
|
|
6
23
|
// ─── Test doubles ────────────────────────────────────────────────────────────
|
|
7
24
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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();
|
|
12
29
|
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
env: {},
|
|
19
|
-
state: {},
|
|
20
|
-
db: noDb,
|
|
21
|
-
generate: () => Promise.reject(new Error("generate not available in tests")),
|
|
22
|
-
messages: [],
|
|
23
|
-
sessionId,
|
|
24
|
-
// Never aborts — a test has no turn to cancel. `ctx.signal` is always
|
|
25
|
-
// present at runtime, so a stub supplies one rather than omitting it.
|
|
26
|
-
signal: new AbortController().signal,
|
|
27
|
-
send: (event, data) => {
|
|
28
|
-
sent.push({ event, data });
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
return { ctx, sent };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getTool(name: string): ToolDef {
|
|
35
|
-
const def = agentDef.tools[name];
|
|
36
|
-
if (!def) throw new Error(`tool ${name} not defined on agent`);
|
|
37
|
-
return def;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function run(name: string, args: Record<string, unknown>, ctx: ToolContext) {
|
|
41
|
-
return await getTool(name).execute(args, ctx);
|
|
42
|
-
}
|
|
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);
|
|
43
35
|
|
|
44
36
|
const margherita: Omit<Pizza, "id"> = {
|
|
45
37
|
size: "medium",
|
|
@@ -80,7 +72,7 @@ describe("pricing (shared.ts)", () => {
|
|
|
80
72
|
});
|
|
81
73
|
|
|
82
74
|
test("add_pizza schema defaults quantity to 1 and rejects non-positive quantities", async () => {
|
|
83
|
-
const schema =
|
|
75
|
+
const schema = toolOf(agentDef, "add_pizza").inputSchema;
|
|
84
76
|
if (!schema) throw new Error("add_pizza has no input schema");
|
|
85
77
|
// Validate through the Standard Schema contract — the vendor-neutral
|
|
86
78
|
// interface every inputSchema carries.
|
|
@@ -99,7 +91,7 @@ describe("pricing (shared.ts)", () => {
|
|
|
99
91
|
|
|
100
92
|
describe("tool flow (add → update → remove → place_order)", () => {
|
|
101
93
|
test("full ordering round-trip keeps state, totals, and IDs consistent", async () => {
|
|
102
|
-
const
|
|
94
|
+
const ctx = makeCtx();
|
|
103
95
|
|
|
104
96
|
// Empty order guards
|
|
105
97
|
expect(await run("view_order", {}, ctx)).toEqual({ message: "The order is empty." });
|
|
@@ -160,31 +152,35 @@ describe("tool flow (add → update → remove → place_order)", () => {
|
|
|
160
152
|
expect(await run("place_order", {}, ctx)).toEqual({ error: "Cannot place an empty order." });
|
|
161
153
|
});
|
|
162
154
|
|
|
163
|
-
test("
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
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();
|
|
167
163
|
|
|
168
164
|
await run(
|
|
169
165
|
"add_pizza",
|
|
170
166
|
{ size: "large", crust: "thin", toppings: ["bacon"], quantity: 1 },
|
|
171
|
-
|
|
167
|
+
firstCall,
|
|
172
168
|
);
|
|
173
|
-
await run("set_customer_name", { name: "Alice" },
|
|
169
|
+
await run("set_customer_name", { name: "Alice" }, firstCall);
|
|
174
170
|
|
|
175
|
-
expect(await run("view_order", {},
|
|
171
|
+
expect(await run("view_order", {}, secondCall)).toEqual({ message: "The order is empty." });
|
|
176
172
|
|
|
177
|
-
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 },
|
|
178
|
-
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 {
|
|
179
175
|
customerName: string;
|
|
180
176
|
pizzas: number;
|
|
181
177
|
};
|
|
182
|
-
//
|
|
178
|
+
// The second context never sees the first's customer name or pizzas.
|
|
183
179
|
expect(placedB.customerName).toBe("Guest");
|
|
184
180
|
expect(placedB.pizzas).toBe(1);
|
|
185
181
|
|
|
186
|
-
//
|
|
187
|
-
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[] };
|
|
188
184
|
expect(viewA.pizzas).toHaveLength(1);
|
|
189
185
|
});
|
|
190
186
|
});
|
|
@@ -197,14 +193,14 @@ describe("tool flow (add → update → remove → place_order)", () => {
|
|
|
197
193
|
|
|
198
194
|
describe("orderView projection", () => {
|
|
199
195
|
test("reflects the live cart", async () => {
|
|
200
|
-
const
|
|
196
|
+
const ctx = makeCtx();
|
|
201
197
|
await run(
|
|
202
198
|
"add_pizza",
|
|
203
199
|
{ size: "large", crust: "stuffed", toppings: ["pepperoni", "extra_cheese"], quantity: 2 },
|
|
204
200
|
ctx,
|
|
205
201
|
);
|
|
206
202
|
|
|
207
|
-
const view = orderView(ctx
|
|
203
|
+
const view = orderView(orderSlot.get(ctx));
|
|
208
204
|
expect(view.orderPlaced).toBe(false);
|
|
209
205
|
const item = view.pizzas[0];
|
|
210
206
|
if (!item) throw new Error("no pizza in the projection");
|
|
@@ -222,19 +218,24 @@ describe("orderView projection", () => {
|
|
|
222
218
|
test("survives checkout, which clears the cart but keeps the confirmation", async () => {
|
|
223
219
|
// The reason `placed` lives in state at all: the cart is emptied on
|
|
224
220
|
// checkout, and the UI still has to show the order that was just placed.
|
|
225
|
-
const
|
|
221
|
+
const ctx = makeCtx();
|
|
226
222
|
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, ctx);
|
|
227
223
|
await run("place_order", {}, ctx);
|
|
228
224
|
|
|
229
|
-
const view = orderView(ctx
|
|
225
|
+
const view = orderView(orderSlot.get(ctx));
|
|
230
226
|
expect(view.orderPlaced).toBe(true);
|
|
231
227
|
expect(view.pizzas).toEqual([]);
|
|
232
228
|
expect(view.estimatedMinutes).toBe(20);
|
|
233
229
|
expect(view.total).toMatch(/^\$\d+\.\d{2}$/);
|
|
234
230
|
});
|
|
235
231
|
|
|
236
|
-
test("an untouched session projects an empty cart, not undefined",
|
|
237
|
-
// The client renders before any tool has run
|
|
238
|
-
|
|
232
|
+
test("an untouched session projects an empty cart, not undefined", () => {
|
|
233
|
+
// The client renders before any tool has run, so `state.order` is absent —
|
|
234
|
+
// this is exactly the value `client.tsx` hoists as its fallback.
|
|
235
|
+
expect(orderSlot.projection(orderView)(undefined)).toMatchObject({
|
|
236
|
+
pizzas: [],
|
|
237
|
+
total: "$0.00",
|
|
238
|
+
orderPlaced: false,
|
|
239
|
+
});
|
|
239
240
|
});
|
|
240
241
|
});
|
|
@@ -1,173 +1,24 @@
|
|
|
1
|
-
import { agent
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
CRUSTS,
|
|
5
|
-
calculateTotal,
|
|
6
|
-
formatPrice,
|
|
7
|
-
getOrder,
|
|
8
|
-
menuText,
|
|
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.
|
|
27
|
-
syncState: orderView,
|
|
18
|
+
syncState: orderSlot.projection(orderView),
|
|
28
19
|
// The menu section is generated from MENU so the prompt can never quote a
|
|
29
20
|
// price the pricing code doesn't charge.
|
|
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 = getOrder(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 = getOrder(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 = getOrder(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
|
-
getOrder(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 = getOrder(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 = getOrder(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,11 +1,11 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
2
|
import { client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
|
|
3
3
|
import type { OrderView } from "./shared.ts";
|
|
4
|
-
import { formatPrice, orderView, pizzaPrice } from "./shared.ts";
|
|
4
|
+
import { formatPrice, orderSlot, orderView, pizzaPrice } from "./shared.ts";
|
|
5
5
|
|
|
6
6
|
// Derived from the projection so a new OrderView field can't silently miss
|
|
7
|
-
// the pre-first-tool-call render.
|
|
8
|
-
const EMPTY_ORDER = orderView(
|
|
7
|
+
// the pre-first-tool-call render — `projection` runs it over an empty cart.
|
|
8
|
+
const EMPTY_ORDER: OrderView = orderSlot.projection(orderView)(undefined);
|
|
9
9
|
|
|
10
10
|
function PizzaIcon({ size }: { size: string }) {
|
|
11
11
|
const dim = size === "small" ? 36 : size === "large" ? 52 : 44;
|
|
@@ -30,7 +30,7 @@ function OrderSidebar() {
|
|
|
30
30
|
// tool call. This replaced ~45 lines that rebuilt the cart by diffing
|
|
31
31
|
// added/removed/updated events — where one missed event desynced the view
|
|
32
32
|
// for the rest of the session.
|
|
33
|
-
const order = useAgentState<OrderView>()
|
|
33
|
+
const order = useAgentState<OrderView>(EMPTY_ORDER);
|
|
34
34
|
|
|
35
35
|
if (order.orderPlaced) {
|
|
36
36
|
return (
|
|
@@ -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[];
|
|
@@ -89,51 +89,68 @@ export interface OrderState {
|
|
|
89
89
|
placed?: { orderNumber: number; total: string; estimatedMinutes: number };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
function emptyOrder(): OrderState {
|
|
92
|
+
export function emptyOrder(): OrderState {
|
|
95
93
|
return { pizzas: [], nextId: 1, customerName: null };
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
/** The session's
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
/** The session's cart, as one typed slot. */
|
|
97
|
+
export const orderSlot = sessionSlot("order", emptyOrder);
|
|
98
|
+
|
|
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>;
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
112
|
* Clear the cart after checkout so a follow-up order starts fresh. The
|
|
108
113
|
* `placed` confirmation is passed through, because the UI is still showing
|
|
109
114
|
* it — clearing the pizzas is what "reset" means here, not forgetting the
|
|
110
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.
|
|
111
122
|
*/
|
|
112
|
-
export function resetOrder(
|
|
113
|
-
const
|
|
114
|
-
|
|
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;
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
/**
|
|
118
|
-
* What the browser sees. A projection rather than the raw
|
|
133
|
+
* What the browser sees. A projection rather than the raw state: the client
|
|
119
134
|
* needs the cart and its total, and `syncState` is where you decide what
|
|
120
135
|
* leaves the server.
|
|
121
136
|
*/
|
|
122
137
|
export interface OrderView {
|
|
123
|
-
|
|
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[];
|
|
124
141
|
total: string;
|
|
125
142
|
orderPlaced: boolean;
|
|
126
143
|
orderNumber?: number;
|
|
127
144
|
estimatedMinutes?: number;
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const placed = order
|
|
147
|
+
/** Takes the cart itself, not the slot — `orderSlot.projection` supplies a real
|
|
148
|
+
* one even before the first tool call, so there is nothing to optional-chain. */
|
|
149
|
+
export function orderView(order: FrozenOrderState): OrderView {
|
|
150
|
+
const placed = order.placed;
|
|
134
151
|
return {
|
|
135
|
-
pizzas,
|
|
136
|
-
total: placed?.total ?? formatPrice(calculateTotal(pizzas)),
|
|
152
|
+
pizzas: order.pizzas,
|
|
153
|
+
total: placed?.total ?? formatPrice(calculateTotal(order.pizzas)),
|
|
137
154
|
orderPlaced: Boolean(placed),
|
|
138
155
|
...(placed
|
|
139
156
|
? { orderNumber: placed.orderNumber, estimatedMinutes: placed.estimatedMinutes }
|
|
@@ -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
|
+
});
|