@alexkroman1/aai-cli 5.13.2 → 5.14.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/scaffold/package.json +3 -3
- package/dist/scaffold/pnpm-workspace.yaml +24 -0
- package/dist/templates/dispatch-center/agent.test.ts +10 -17
- package/dist/templates/dispatch-center/agent.ts +2 -2
- package/dist/templates/dispatch-center/client.tsx +9 -13
- package/dist/templates/dispatch-center/shared.ts +36 -63
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_create.ts +2 -2
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_get.ts +9 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +4 -4
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -2
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +4 -4
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +2 -2
- package/dist/templates/infocom-adventure/agent.ts +10 -10
- package/dist/templates/infocom-adventure/client.tsx +5 -17
- package/dist/templates/infocom-adventure/shared.ts +17 -14
- package/dist/templates/pizza-ordering/agent.test.ts +27 -31
- package/dist/templates/pizza-ordering/agent.ts +8 -8
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +14 -20
- package/dist/templates/retail/agent.test.ts +137 -138
- package/dist/templates/retail/agent.ts +7 -10
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +13 -14
- package/dist/templates/retail/registry.test.ts +9 -9
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +5 -4
- package/dist/templates/retail/shared.test.ts +17 -16
- package/dist/templates/retail/shared.ts +39 -18
- package/dist/templates/retail/store.test.ts +40 -47
- package/dist/templates/retail/store.ts +39 -65
- 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 +5 -4
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +6 -5
- package/dist/templates/retail/tools/find_user_id_by_email.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +2 -2
- package/dist/templates/retail/tools/get_item_details.ts +4 -3
- package/dist/templates/retail/tools/get_order_details.ts +4 -3
- package/dist/templates/retail/tools/get_product_details.ts +4 -3
- package/dist/templates/retail/tools/get_user_details.ts +4 -3
- package/dist/templates/retail/tools/list_all_product_types.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -3
- package/dist/templates/retail/tools/modify_pending_order_items.ts +6 -6
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +6 -6
- package/dist/templates/retail/tools/modify_user_address.ts +4 -3
- package/dist/templates/retail/tools/return_delivered_order_items.ts +6 -6
- package/dist/templates/solo-rpg/agent.test.ts +26 -34
- package/dist/templates/solo-rpg/agent.ts +2 -2
- package/dist/templates/solo-rpg/client.tsx +1 -1
- package/dist/templates/solo-rpg/shared.ts +6 -14
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -2
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -2
- package/dist/templates/solo-rpg/tools/check_state.ts +2 -2
- package/dist/templates/solo-rpg/tools/load_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/oracle.ts +3 -3
- package/dist/templates/solo-rpg/tools/save_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/setup_character.ts +2 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +12 -11
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { gameSlot, recordCommand } from "./shared.ts";
|
|
4
4
|
import systemPrompt from "./system-prompt.md?raw";
|
|
5
5
|
|
|
6
6
|
export default agent({
|
|
@@ -21,7 +21,7 @@ export default agent({
|
|
|
21
21
|
value: z.string().describe("Item name to drop"),
|
|
22
22
|
}),
|
|
23
23
|
async execute(args, ctx) {
|
|
24
|
-
const g =
|
|
24
|
+
const g = gameSlot.get(ctx);
|
|
25
25
|
g.inventory = g.inventory.filter((i) => i !== args.value);
|
|
26
26
|
return { inventory: g.inventory };
|
|
27
27
|
},
|
|
@@ -33,7 +33,7 @@ export default agent({
|
|
|
33
33
|
value: z.string().describe("Flag name to set"),
|
|
34
34
|
}),
|
|
35
35
|
async execute(args, ctx) {
|
|
36
|
-
const g =
|
|
36
|
+
const g = gameSlot.get(ctx);
|
|
37
37
|
g.flags[args.value] = true;
|
|
38
38
|
return { flags: g.flags };
|
|
39
39
|
},
|
|
@@ -43,7 +43,7 @@ export default agent({
|
|
|
43
43
|
description:
|
|
44
44
|
"Read the current game state including inventory, current room, score, moves, flags, and recent history.",
|
|
45
45
|
async execute(_args, ctx) {
|
|
46
|
-
const g =
|
|
46
|
+
const g = gameSlot.get(ctx);
|
|
47
47
|
return {
|
|
48
48
|
currentRoom: g.currentRoom,
|
|
49
49
|
inventory: g.inventory,
|
|
@@ -61,8 +61,8 @@ export default agent({
|
|
|
61
61
|
value: z.string().describe("Command text to log"),
|
|
62
62
|
}),
|
|
63
63
|
async execute(args, ctx) {
|
|
64
|
-
const g =
|
|
65
|
-
g
|
|
64
|
+
const g = gameSlot.get(ctx);
|
|
65
|
+
recordCommand(g, args.value);
|
|
66
66
|
g.moves++;
|
|
67
67
|
return { moves: g.moves, recentHistory: g.history.slice(-5) };
|
|
68
68
|
},
|
|
@@ -74,7 +74,7 @@ export default agent({
|
|
|
74
74
|
value: z.string().describe("Room name to move to"),
|
|
75
75
|
}),
|
|
76
76
|
async execute(args, ctx) {
|
|
77
|
-
const g =
|
|
77
|
+
const g = gameSlot.get(ctx);
|
|
78
78
|
g.currentRoom = args.value;
|
|
79
79
|
g.moves++;
|
|
80
80
|
return { currentRoom: g.currentRoom, moves: g.moves };
|
|
@@ -85,7 +85,7 @@ export default agent({
|
|
|
85
85
|
description:
|
|
86
86
|
"Reset the game to the beginning: empty inventory, zero score and moves, all flags cleared. Use when the player asks to restart, quit, or start a new game.",
|
|
87
87
|
async execute(_args, ctx) {
|
|
88
|
-
const g =
|
|
88
|
+
const g = gameSlot.reset(ctx);
|
|
89
89
|
return { restarted: true, currentRoom: g.currentRoom };
|
|
90
90
|
},
|
|
91
91
|
}),
|
|
@@ -96,7 +96,7 @@ export default agent({
|
|
|
96
96
|
value: z.number().describe("Points to add"),
|
|
97
97
|
}),
|
|
98
98
|
async execute(args, ctx) {
|
|
99
|
-
const g =
|
|
99
|
+
const g = gameSlot.get(ctx);
|
|
100
100
|
g.score += args.value;
|
|
101
101
|
return { score: g.score };
|
|
102
102
|
},
|
|
@@ -108,7 +108,7 @@ export default agent({
|
|
|
108
108
|
value: z.string().describe("Item name to take"),
|
|
109
109
|
}),
|
|
110
110
|
async execute(args, ctx) {
|
|
111
|
-
const g =
|
|
111
|
+
const g = gameSlot.get(ctx);
|
|
112
112
|
if (!g.inventory.includes(args.value)) g.inventory.push(args.value);
|
|
113
113
|
return { inventory: g.inventory };
|
|
114
114
|
},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
2
|
import type { ChatMessage } from "@alexkroman1/aai-ui";
|
|
3
|
-
import { client, useSession } from "@alexkroman1/aai-ui";
|
|
4
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { AutoScroll, client, useSession } from "@alexkroman1/aai-ui";
|
|
5
4
|
|
|
6
5
|
const CSS = `
|
|
7
6
|
@keyframes ic-flicker {
|
|
@@ -55,17 +54,6 @@ const CYAN = "#00ccff";
|
|
|
55
54
|
|
|
56
55
|
function InfocomAdventure() {
|
|
57
56
|
const session = useSession();
|
|
58
|
-
const bottom = useRef<HTMLDivElement>(null);
|
|
59
|
-
|
|
60
|
-
// Bumps whenever a message lands or the live transcript changes, so the
|
|
61
|
-
// view follows the conversation instead of scrolling once on mount.
|
|
62
|
-
const contentVersion = session.messages.length + (session.userTranscript?.length ?? 0);
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
// Never true — contentVersion only drives the dependency array, and this
|
|
65
|
-
// reference keeps the exhaustive-deps lint satisfied.
|
|
66
|
-
if (contentVersion < 0) return;
|
|
67
|
-
bottom.current?.scrollIntoView({ behavior: "smooth" });
|
|
68
|
-
}, [contentVersion]);
|
|
69
57
|
|
|
70
58
|
const stateLabel =
|
|
71
59
|
session.state === "listening"
|
|
@@ -184,8 +172,9 @@ function InfocomAdventure() {
|
|
|
184
172
|
)}
|
|
185
173
|
|
|
186
174
|
{/* Messages */}
|
|
187
|
-
<
|
|
188
|
-
|
|
175
|
+
<AutoScroll
|
|
176
|
+
scrollClassName="ic-messages overflow-y-auto"
|
|
177
|
+
contentClassName="p-5"
|
|
189
178
|
style={{ scrollbarWidth: "thin", scrollbarColor: `${GREEN} #001a00` }}
|
|
190
179
|
>
|
|
191
180
|
{session.messages.map((msg: ChatMessage, i: number) => (
|
|
@@ -211,8 +200,7 @@ function InfocomAdventure() {
|
|
|
211
200
|
{session.userTranscript === "" ? "..." : session.userTranscript}
|
|
212
201
|
</div>
|
|
213
202
|
)}
|
|
214
|
-
|
|
215
|
-
</div>
|
|
203
|
+
</AutoScroll>
|
|
216
204
|
|
|
217
205
|
{/* Footer controls */}
|
|
218
206
|
<div
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { pushCapped, type SlotStateOf, sessionSlot } from "@alexkroman1/aai";
|
|
2
2
|
|
|
3
3
|
export type GameState = {
|
|
4
4
|
inventory: string[];
|
|
@@ -18,21 +18,24 @@ export const DEFAULT_GAME_STATE: GameState = {
|
|
|
18
18
|
history: [],
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* How many player commands the game remembers. The history is append-only and
|
|
23
|
+
* lives in `ctx.state` for the length of the call, so it needs a cap — and
|
|
24
|
+
* only the last few are ever read (`game_state_get` reports five), so the
|
|
25
|
+
* older ones are cost without a reader.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_HISTORY = 50;
|
|
28
|
+
|
|
21
29
|
// The game lives in `ctx.state`, the agent's per-session mutable state — each
|
|
22
30
|
// session is its own playthrough, so concurrent players never see each
|
|
23
|
-
// other's game and a fresh session starts a fresh adventure.
|
|
24
|
-
|
|
31
|
+
// other's game and a fresh session starts a fresh adventure. The clone is
|
|
32
|
+
// load-bearing: `DEFAULT_GAME_STATE` is one module-level object shared by
|
|
33
|
+
// every session in the process.
|
|
34
|
+
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_GAME_STATE));
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
* the object stored in `ctx.state`. */
|
|
28
|
-
export function getGameState(ctx: ToolContext): GameState {
|
|
29
|
-
const slot = ctx.state as StateSlot;
|
|
30
|
-
slot.game ??= structuredClone(DEFAULT_GAME_STATE);
|
|
31
|
-
return slot.game;
|
|
32
|
-
}
|
|
36
|
+
export type StateSlot = SlotStateOf<typeof gameSlot>;
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return slot.game;
|
|
38
|
+
/** Log a player command, holding {@link MAX_HISTORY}. */
|
|
39
|
+
export function recordCommand(game: GameState, command: string): void {
|
|
40
|
+
pushCapped(game.history, command, MAX_HISTORY);
|
|
38
41
|
}
|
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ToolContext, ToolDef } from "@alexkroman1/aai";
|
|
2
|
+
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
2
3
|
import { describe, expect, test } from "vitest";
|
|
3
4
|
import agentDef from "./agent.ts";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
calculateTotal,
|
|
7
|
+
orderSlot,
|
|
8
|
+
orderView,
|
|
9
|
+
type Pizza,
|
|
10
|
+
pizzaPrice,
|
|
11
|
+
type StateSlot,
|
|
12
|
+
} from "./shared.ts";
|
|
5
13
|
|
|
6
14
|
// ─── Test doubles ────────────────────────────────────────────────────────────
|
|
7
15
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function makeCtx(sessionId = "session-a") {
|
|
16
|
-
const sent: Array<{ event: string; data: unknown }> = [];
|
|
17
|
-
const ctx: ToolContext = {
|
|
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 };
|
|
16
|
+
/** Each context is one session — the cart is session-scoped by construction,
|
|
17
|
+
* and `createToolContext` mints a distinct session id per call. Its default
|
|
18
|
+
* `db` rejects every query, which is right here: this template keeps its cart
|
|
19
|
+
* in ctx.state and must never touch storage. */
|
|
20
|
+
function makeCtx(sessionId?: string) {
|
|
21
|
+
const ctx = createToolContext<StateSlot>(sessionId ? { sessionId } : {});
|
|
22
|
+
return { ctx, sent: ctx.sent };
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
function getTool(name: string): ToolDef {
|
|
@@ -204,7 +195,7 @@ describe("orderView projection", () => {
|
|
|
204
195
|
ctx,
|
|
205
196
|
);
|
|
206
197
|
|
|
207
|
-
const view = orderView(ctx.state
|
|
198
|
+
const view = orderView(orderSlot.read(ctx.state));
|
|
208
199
|
expect(view.orderPlaced).toBe(false);
|
|
209
200
|
const item = view.pizzas[0];
|
|
210
201
|
if (!item) throw new Error("no pizza in the projection");
|
|
@@ -226,15 +217,20 @@ describe("orderView projection", () => {
|
|
|
226
217
|
await run("add_pizza", { size: "small", crust: "thin", toppings: [], quantity: 1 }, ctx);
|
|
227
218
|
await run("place_order", {}, ctx);
|
|
228
219
|
|
|
229
|
-
const view = orderView(ctx.state
|
|
220
|
+
const view = orderView(orderSlot.read(ctx.state));
|
|
230
221
|
expect(view.orderPlaced).toBe(true);
|
|
231
222
|
expect(view.pizzas).toEqual([]);
|
|
232
223
|
expect(view.estimatedMinutes).toBe(20);
|
|
233
224
|
expect(view.total).toMatch(/^\$\d+\.\d{2}$/);
|
|
234
225
|
});
|
|
235
226
|
|
|
236
|
-
test("an untouched session projects an empty cart, not undefined",
|
|
237
|
-
// The client renders before any tool has run
|
|
238
|
-
|
|
227
|
+
test("an untouched session projects an empty cart, not undefined", () => {
|
|
228
|
+
// The client renders before any tool has run, so `state.order` is absent —
|
|
229
|
+
// this is exactly the value `client.tsx` hoists as its fallback.
|
|
230
|
+
expect(orderSlot.projection(orderView)(undefined)).toMatchObject({
|
|
231
|
+
pizzas: [],
|
|
232
|
+
total: "$0.00",
|
|
233
|
+
orderPlaced: false,
|
|
234
|
+
});
|
|
239
235
|
});
|
|
240
236
|
});
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
CRUSTS,
|
|
5
5
|
calculateTotal,
|
|
6
6
|
formatPrice,
|
|
7
|
-
getOrder,
|
|
8
7
|
menuText,
|
|
8
|
+
orderSlot,
|
|
9
9
|
orderView,
|
|
10
10
|
type Pizza,
|
|
11
11
|
resetOrder,
|
|
@@ -24,7 +24,7 @@ export default agent({
|
|
|
24
24
|
// The cart, pushed to the client after every tool call. Replaces a
|
|
25
25
|
// `ctx.send("order", ...)` in each of the five order tools, and the
|
|
26
26
|
// event-diffing the client had to do to rebuild the cart from them.
|
|
27
|
-
syncState: orderView,
|
|
27
|
+
syncState: orderSlot.projection(orderView),
|
|
28
28
|
// The menu section is generated from MENU so the prompt can never quote a
|
|
29
29
|
// price the pricing code doesn't charge.
|
|
30
30
|
systemPrompt: `${systemPrompt}\n${menuText()}`,
|
|
@@ -43,7 +43,7 @@ export default agent({
|
|
|
43
43
|
quantity: z.number().int().min(1).default(1),
|
|
44
44
|
}),
|
|
45
45
|
async execute(args, ctx) {
|
|
46
|
-
const order =
|
|
46
|
+
const order = orderSlot.get(ctx);
|
|
47
47
|
|
|
48
48
|
const pizza: Pizza = {
|
|
49
49
|
id: order.nextId,
|
|
@@ -67,7 +67,7 @@ export default agent({
|
|
|
67
67
|
description:
|
|
68
68
|
"Place the final order. Use when the customer confirms they are done and ready to order.",
|
|
69
69
|
async execute(_args, ctx) {
|
|
70
|
-
const order =
|
|
70
|
+
const order = orderSlot.get(ctx);
|
|
71
71
|
const pizzas = order.pizzas;
|
|
72
72
|
if (pizzas.length === 0) return { error: "Cannot place an empty order." };
|
|
73
73
|
|
|
@@ -96,7 +96,7 @@ export default agent({
|
|
|
96
96
|
pizza_id: z.number().describe("The pizza ID to remove"),
|
|
97
97
|
}),
|
|
98
98
|
async execute(args, ctx) {
|
|
99
|
-
const order =
|
|
99
|
+
const order = orderSlot.get(ctx);
|
|
100
100
|
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
101
101
|
if (idx === -1) return { error: "Pizza not found in the order." };
|
|
102
102
|
|
|
@@ -116,7 +116,7 @@ export default agent({
|
|
|
116
116
|
name: z.string(),
|
|
117
117
|
}),
|
|
118
118
|
async execute(args, ctx) {
|
|
119
|
-
|
|
119
|
+
orderSlot.get(ctx).customerName = args.name;
|
|
120
120
|
return { name: args.name };
|
|
121
121
|
},
|
|
122
122
|
}),
|
|
@@ -131,7 +131,7 @@ export default agent({
|
|
|
131
131
|
quantity: z.number().int().min(1).optional(),
|
|
132
132
|
}),
|
|
133
133
|
async execute(args, ctx) {
|
|
134
|
-
const order =
|
|
134
|
+
const order = orderSlot.get(ctx);
|
|
135
135
|
const idx = order.pizzas.findIndex((p) => p.id === args.pizza_id);
|
|
136
136
|
if (idx === -1) return { error: "Pizza not found in the order." };
|
|
137
137
|
|
|
@@ -153,7 +153,7 @@ export default agent({
|
|
|
153
153
|
view_order: tool({
|
|
154
154
|
description: "View the current order summary with all pizzas and total price.",
|
|
155
155
|
async execute(_args, ctx) {
|
|
156
|
-
const pizzas =
|
|
156
|
+
const pizzas = orderSlot.get(ctx).pizzas;
|
|
157
157
|
if (pizzas.length === 0) return { message: "The order is empty." };
|
|
158
158
|
|
|
159
159
|
return {
|
|
@@ -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 SlotStateOf, sessionSlot, type ToolContext } 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;
|
|
@@ -89,19 +89,14 @@ 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
|
-
slot.order ??= emptyOrder();
|
|
103
|
-
return slot.order;
|
|
104
|
-
}
|
|
96
|
+
/** The session's cart, as one typed slot inside `ctx.state`. */
|
|
97
|
+
export const orderSlot = sessionSlot("order", emptyOrder);
|
|
98
|
+
|
|
99
|
+
export type StateSlot = SlotStateOf<typeof orderSlot>;
|
|
105
100
|
|
|
106
101
|
/**
|
|
107
102
|
* Clear the cart after checkout so a follow-up order starts fresh. The
|
|
@@ -110,12 +105,11 @@ export function getOrder(ctx: ToolContext): OrderState {
|
|
|
110
105
|
* order that was just submitted.
|
|
111
106
|
*/
|
|
112
107
|
export function resetOrder(ctx: ToolContext, placed?: OrderState["placed"]): void {
|
|
113
|
-
|
|
114
|
-
slot.order = { ...emptyOrder(), ...(placed ? { placed } : {}) };
|
|
108
|
+
orderSlot.set(ctx, { ...emptyOrder(), ...(placed ? { placed } : {}) });
|
|
115
109
|
}
|
|
116
110
|
|
|
117
111
|
/**
|
|
118
|
-
* What the browser sees. A projection rather than the raw
|
|
112
|
+
* What the browser sees. A projection rather than the raw state: the client
|
|
119
113
|
* needs the cart and its total, and `syncState` is where you decide what
|
|
120
114
|
* leaves the server.
|
|
121
115
|
*/
|
|
@@ -127,13 +121,13 @@ export interface OrderView {
|
|
|
127
121
|
estimatedMinutes?: number;
|
|
128
122
|
}
|
|
129
123
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const placed = order
|
|
124
|
+
/** Takes the cart itself, not the slot — `orderSlot.projection` supplies a real
|
|
125
|
+
* one even before the first tool call, so there is nothing to optional-chain. */
|
|
126
|
+
export function orderView(order: OrderState): OrderView {
|
|
127
|
+
const placed = order.placed;
|
|
134
128
|
return {
|
|
135
|
-
pizzas,
|
|
136
|
-
total: placed?.total ?? formatPrice(calculateTotal(pizzas)),
|
|
129
|
+
pizzas: order.pizzas,
|
|
130
|
+
total: placed?.total ?? formatPrice(calculateTotal(order.pizzas)),
|
|
137
131
|
orderPlaced: Boolean(placed),
|
|
138
132
|
...(placed
|
|
139
133
|
? { orderNumber: placed.orderNumber, estimatedMinutes: placed.estimatedMinutes }
|