@alexkroman1/aai-cli 7.0.0 → 8.0.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/README.md +1 -0
- package/dist/{_bundler-B4RqNF5Z.mjs → _bundler-CDuYl5Gb.mjs} +1 -1
- package/dist/{_dev-server-CiKFH9qw.mjs → _dev-server-Me06wgeF.mjs} +2 -2
- package/dist/{_init-CE4HKdgV.mjs → _init-BhEfQ6Yi.mjs} +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-nRFaBEPJ.mjs → _slug-api-z-USxPtL.mjs} +14 -10
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_templates-FI3xPEMj.mjs → _templates-4WcKOjS5.mjs} +4 -5
- package/dist/_templates.d.ts +3 -4
- package/dist/{build-6jfaakgc.mjs → build-BBxf8vom.mjs} +2 -2
- package/dist/cli.mjs +338 -346
- package/dist/{deploy-DvYeD8ia.mjs → deploy-DRC8jWr8.mjs} +1 -1
- package/dist/{dev-B59JIfWO.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-CpJVjEg0.mjs → init-CUo87gcI.mjs} +2 -2
- package/dist/{logs-9q-psa4q.mjs → logs-CQ2T46dr.mjs} +1 -1
- package/dist/scaffold/CLAUDE.md +409 -68
- package/dist/scaffold/package.json +5 -4
- package/dist/scaffold/server.mjs +10 -3
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-CVvSLIDV.mjs → secret-Ct34kdgU.mjs} +1 -1
- package/dist/{storage-BvUrnvM3.mjs → storage-i_79ZwAR.mjs} +1 -1
- package/dist/{studio--MUV0cid.mjs → studio-CzTzPqe1.mjs} +1 -1
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +49 -18
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +15 -0
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +30 -59
- package/dist/templates/podcast-digest/agent.ts +1 -1
- package/dist/templates/podcast-digest/workflows/digest.ts +52 -19
- package/dist/templates/podcast-digest/workflows/slack.ts +48 -169
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +171 -91
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +39 -0
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +119 -8
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +6 -4
- package/dist/templates/retail/store.ts +113 -57
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +62 -30
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +15 -59
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +21 -66
- package/dist/templates/retail/tools/modify_pending_order_address.ts +12 -24
- package/dist/templates/retail/tools/modify_pending_order_items.ts +22 -60
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +15 -77
- package/dist/templates/retail/tools/modify_user_address.ts +12 -15
- package/dist/templates/retail/tools/return_delivered_order_items.ts +16 -71
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +4 -2
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +56 -7
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +110 -10
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/shared.ts +110 -9
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +6 -3
- package/dist/templates/travel-concierge/tools/book_excursion.ts +6 -3
- package/dist/templates/travel-concierge/tools/book_hotel.ts +6 -3
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +6 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +6 -3
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-COxnqstQ.mjs → worker-bundler-CCVEDjm1.mjs} +45 -25
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-D2AQf2Pl.mjs → workflow-BflATMmU.mjs} +19 -11
- package/package.json +5 -5
- package/dist/eject-C9WJyyr2.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
activeAssistant,
|
|
29
29
|
FLIGHTS,
|
|
30
30
|
gateFlow,
|
|
31
|
+
type SpecialistId,
|
|
31
32
|
tripProjection,
|
|
32
33
|
tripSlot,
|
|
33
34
|
tripView,
|
|
@@ -50,6 +51,20 @@ function stateOf(ctx: ToolContext) {
|
|
|
50
51
|
return tripSlot.get(ctx);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Put the call at a desk the way the model has to: through that desk's own
|
|
56
|
+
* delegation tool.
|
|
57
|
+
*
|
|
58
|
+
* Every desk tool checks the stack first (`requireDesk`) and refuses from
|
|
59
|
+
* anywhere else, so a spec driving one has to stand where the model would. It is
|
|
60
|
+
* one line per test rather than a shared `beforeEach` deliberately — which desk
|
|
61
|
+
* a tool belongs to is what the block below is about, so it stays visible at the
|
|
62
|
+
* call site.
|
|
63
|
+
*/
|
|
64
|
+
const atDesk = async (id: SpecialistId, ctx: ToolContext): Promise<void> => {
|
|
65
|
+
await run(`to_${id}_assistant`, { request: "what the caller asked for" }, ctx);
|
|
66
|
+
};
|
|
67
|
+
|
|
53
68
|
// ─── 1. The dialog stack ─────────────────────────────────────────────────────
|
|
54
69
|
|
|
55
70
|
describe("dialog stack (routing.ts)", () => {
|
|
@@ -98,6 +113,72 @@ describe("dialog stack (routing.ts)", () => {
|
|
|
98
113
|
});
|
|
99
114
|
});
|
|
100
115
|
|
|
116
|
+
// ─── 1b. The desk gate ───────────────────────────────────────────────────────
|
|
117
|
+
//
|
|
118
|
+
// What their graph gets from binding a tool set per node, and what this port
|
|
119
|
+
// used to only ask the prompt for. The eval (`agent.eval.test.ts`) is what
|
|
120
|
+
// measured the asking losing — 0 of 5 live runs, the model searching hotels
|
|
121
|
+
// while the stack said `primary` — so these are the tests that make the stack
|
|
122
|
+
// binding rather than descriptive.
|
|
123
|
+
|
|
124
|
+
describe("the desk gate (requireDesk)", () => {
|
|
125
|
+
const DESK_TOOLS = [
|
|
126
|
+
["search_flights", { route: "Boston" }, "flight"],
|
|
127
|
+
["update_ticket", { flightId: "LX52" }, "flight"],
|
|
128
|
+
["cancel_ticket", {}, "flight"],
|
|
129
|
+
["search_hotels", { city: "Boston" }, "hotel"],
|
|
130
|
+
["book_hotel", { hotelId: "H1", nights: 2 }, "hotel"],
|
|
131
|
+
["search_car_rentals", { city: "Boston" }, "car_rental"],
|
|
132
|
+
["book_car_rental", { carId: "C2", days: 3 }, "car_rental"],
|
|
133
|
+
["search_excursions", { city: "Boston" }, "excursion"],
|
|
134
|
+
["book_excursion", { excursionId: "E2" }, "excursion"],
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
test.each(DESK_TOOLS)("%s refuses at the concierge desk", async (name, args, desk) => {
|
|
138
|
+
const ctx = makeCtx();
|
|
139
|
+
const refused = (await run(name, args, ctx)) as { error?: string };
|
|
140
|
+
// The refusal NAMES the way in, which is what the model recovers from
|
|
141
|
+
// inside the same turn — a bare "not allowed" would leave it guessing.
|
|
142
|
+
expect(refused.error).toContain(`to_${desk}_assistant`);
|
|
143
|
+
// And nothing happened: no search result, no staging, no stack movement.
|
|
144
|
+
expect(stateOf(ctx).pending).toBeNull();
|
|
145
|
+
expect(activeAssistant(stateOf(ctx))).toBe("primary");
|
|
146
|
+
expect(gateFlow.position(ctx).state).toBe("browsing");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test.each(DESK_TOOLS)("%s refuses from ANOTHER desk", async (name, args, desk) => {
|
|
150
|
+
const ctx = makeCtx();
|
|
151
|
+
// The excursions desk for anything that is not its own, and the flight desk
|
|
152
|
+
// for the excursion tools — so every case really is a wrong desk rather
|
|
153
|
+
// than a coincidence of ordering.
|
|
154
|
+
const wrong = desk === "excursion" ? "flight" : "excursion";
|
|
155
|
+
await atDesk(wrong, ctx);
|
|
156
|
+
const refused = (await run(name, args, ctx)) as { error?: string };
|
|
157
|
+
expect(refused.error).toContain(`to_${desk}_assistant`);
|
|
158
|
+
expect(refused.error).toContain(desk === "excursion" ? "flight desk" : "excursions desk");
|
|
159
|
+
expect(stateOf(ctx).pending).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test.each(DESK_TOOLS)("%s works once its own desk holds the call", async (name, args, desk) => {
|
|
163
|
+
// The other half, and the one that makes the block above non-vacuous: a
|
|
164
|
+
// gate that refused everywhere would pass every assertion up to here.
|
|
165
|
+
const ctx = makeCtx();
|
|
166
|
+
await atDesk(desk, ctx);
|
|
167
|
+
expect(await run(name, args, ctx)).not.toMatchObject({
|
|
168
|
+
error: expect.stringContaining("belongs to the"),
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("the concierge's own tools are NOT gated", async () => {
|
|
173
|
+
// `lookup_booking` is the concierge's, and the gate must not make the desk
|
|
174
|
+
// unable to answer "what am I holding?" from wherever the call is.
|
|
175
|
+
const ctx = makeCtx();
|
|
176
|
+
await atDesk("hotel", ctx);
|
|
177
|
+
const booking = (await run("lookup_booking", ctx)) as { passenger: string };
|
|
178
|
+
expect(booking.passenger).toBe("Nadia Rossi");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
101
182
|
// ─── 2. The confirmation gate ────────────────────────────────────────────────
|
|
102
183
|
//
|
|
103
184
|
// Their graph halts before a sensitive tool (`interrupt_before`) and resumes on
|
|
@@ -107,13 +188,14 @@ describe("dialog stack (routing.ts)", () => {
|
|
|
107
188
|
|
|
108
189
|
describe("sensitive tools stage rather than act", () => {
|
|
109
190
|
test.each([
|
|
110
|
-
["update_ticket", { flightId: "LX52" }],
|
|
111
|
-
["cancel_ticket", {}],
|
|
112
|
-
["book_hotel", { hotelId: "H1", nights: 2 }],
|
|
113
|
-
["book_car_rental", { carId: "C2", days: 3 }],
|
|
114
|
-
["book_excursion", { excursionId: "E2" }],
|
|
115
|
-
])("%s changes nothing on its own", async (name, args) => {
|
|
191
|
+
["update_ticket", { flightId: "LX52" }, "flight"],
|
|
192
|
+
["cancel_ticket", {}, "flight"],
|
|
193
|
+
["book_hotel", { hotelId: "H1", nights: 2 }, "hotel"],
|
|
194
|
+
["book_car_rental", { carId: "C2", days: 3 }, "car_rental"],
|
|
195
|
+
["book_excursion", { excursionId: "E2" }, "excursion"],
|
|
196
|
+
] as const)("%s changes nothing on its own", async (name, args, desk) => {
|
|
116
197
|
const ctx = makeCtx();
|
|
198
|
+
await atDesk(desk, ctx);
|
|
117
199
|
const before = structuredClone(stateOf(ctx));
|
|
118
200
|
|
|
119
201
|
const staged = (await run(name, args, ctx)) as { awaitingConfirmation: boolean };
|
|
@@ -127,6 +209,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
127
209
|
|
|
128
210
|
test("confirm_action applies the staged change, and only then", async () => {
|
|
129
211
|
const ctx = makeCtx();
|
|
212
|
+
await atDesk("hotel", ctx);
|
|
130
213
|
await run("book_hotel", { hotelId: "H1", nights: 3 }, ctx);
|
|
131
214
|
|
|
132
215
|
// Staging moved the gate, in the same window it wrote `pending`.
|
|
@@ -160,6 +243,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
160
243
|
|
|
161
244
|
test("cancel_action drops the staged change and leaves the booking alone", async () => {
|
|
162
245
|
const ctx = makeCtx();
|
|
246
|
+
await atDesk("flight", ctx);
|
|
163
247
|
await run("update_ticket", { flightId: "LX54" }, ctx);
|
|
164
248
|
const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
|
|
165
249
|
expect(dropped.result.discarded).toContain("LX54");
|
|
@@ -177,6 +261,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
177
261
|
// Refused where the model can still recover — before the caller is asked to
|
|
178
262
|
// confirm a flight the airline does not fly.
|
|
179
263
|
const ctx = makeCtx();
|
|
264
|
+
await atDesk("flight", ctx);
|
|
180
265
|
expect(await run("update_ticket", { flightId: "ZZ99" }, ctx)).toEqual({
|
|
181
266
|
error: "No flight ZZ99 in the schedule.",
|
|
182
267
|
});
|
|
@@ -185,6 +270,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
185
270
|
|
|
186
271
|
test("cancelling the ticket makes a later ticket change impossible", async () => {
|
|
187
272
|
const ctx = makeCtx();
|
|
273
|
+
await atDesk("flight", ctx);
|
|
188
274
|
await run("cancel_ticket", ctx);
|
|
189
275
|
await run("confirm_action", ctx);
|
|
190
276
|
expect(stateOf(ctx).ticket).toBeNull();
|
|
@@ -197,17 +283,22 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
197
283
|
|
|
198
284
|
test("a second staging is REFUSED rather than overwriting the first", async () => {
|
|
199
285
|
// The LLM loop runs a step's tool calls concurrently, so two sensitive
|
|
200
|
-
// tools in one step is ordinary: the model hears "move my flight and
|
|
201
|
-
// the
|
|
286
|
+
// tools in one step is ordinary: the model hears "move my flight and cancel
|
|
287
|
+
// the old one" and emits both. Assigning `pending` unconditionally made the
|
|
202
288
|
// second win — both answered `awaitingConfirmation`, the caller said yes
|
|
203
289
|
// once, and one of the two changes was silently dropped forever.
|
|
204
290
|
const ctx = makeCtx();
|
|
291
|
+
await atDesk("flight", ctx);
|
|
205
292
|
const first = (await run("update_ticket", { flightId: "LX52" }, ctx)) as {
|
|
206
293
|
awaitingConfirmation: boolean;
|
|
207
294
|
};
|
|
208
295
|
expect(first.awaitingConfirmation).toBe(true);
|
|
209
296
|
|
|
210
|
-
|
|
297
|
+
// Both from ONE desk, which is what a concurrent pair looks like now that
|
|
298
|
+
// `requireDesk` is enforced. Another desk's tool would be refused by the
|
|
299
|
+
// DESK gate first, which is a different refusal and would leave this one
|
|
300
|
+
// unexercised.
|
|
301
|
+
const second = (await run("cancel_ticket", ctx)) as {
|
|
211
302
|
error: string;
|
|
212
303
|
};
|
|
213
304
|
// The refusal NAMES what is already waiting, which is what lets the model
|
|
@@ -226,7 +317,8 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
226
317
|
expect(applied.applied).toContain("LX52");
|
|
227
318
|
expect(stateOf(ctx).bookings).toEqual([]);
|
|
228
319
|
|
|
229
|
-
// Once the queue is clear, the
|
|
320
|
+
// Once the queue is clear, the next desk's booking can be staged after all.
|
|
321
|
+
await atDesk("hotel", ctx);
|
|
230
322
|
const retried = (await run("book_hotel", { hotelId: "H1", nights: 2 }, ctx)) as {
|
|
231
323
|
awaitingConfirmation: boolean;
|
|
232
324
|
};
|
|
@@ -235,8 +327,10 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
235
327
|
|
|
236
328
|
test("cancel_action clears the block, so a declined change does not wedge the desk", async () => {
|
|
237
329
|
const ctx = makeCtx();
|
|
330
|
+
await atDesk("car_rental", ctx);
|
|
238
331
|
await run("book_car_rental", { carId: "C2", days: 3 }, ctx);
|
|
239
332
|
await run("cancel_action", ctx);
|
|
333
|
+
await atDesk("excursion", ctx);
|
|
240
334
|
const staged = (await run("book_excursion", { excursionId: "E2" }, ctx)) as {
|
|
241
335
|
awaitingConfirmation: boolean;
|
|
242
336
|
};
|
|
@@ -272,6 +366,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
272
366
|
describe("search tools", () => {
|
|
273
367
|
test("an unmatched route widens to the whole schedule rather than answering nothing", async () => {
|
|
274
368
|
const ctx = makeCtx();
|
|
369
|
+
await atDesk("flight", ctx);
|
|
275
370
|
const hit = (await run("search_flights", { route: "Zurich to Boston" }, ctx)) as {
|
|
276
371
|
widened: boolean;
|
|
277
372
|
flights: unknown[];
|
|
@@ -289,6 +384,7 @@ describe("search tools", () => {
|
|
|
289
384
|
|
|
290
385
|
test("hotels come back cheapest first, and a ceiling filters them", async () => {
|
|
291
386
|
const ctx = makeCtx();
|
|
387
|
+
await atDesk("hotel", ctx);
|
|
292
388
|
const all = (await run("search_hotels", { city: "Boston" }, ctx)) as {
|
|
293
389
|
hotels: { perNight: string }[];
|
|
294
390
|
};
|
|
@@ -303,6 +399,7 @@ describe("search tools", () => {
|
|
|
303
399
|
|
|
304
400
|
test("an excursion keyword that matches nothing falls back to the city", async () => {
|
|
305
401
|
const ctx = makeCtx();
|
|
402
|
+
await atDesk("excursion", ctx);
|
|
306
403
|
const result = (await run("search_excursions", { city: "Boston", keyword: "skiing" }, ctx)) as {
|
|
307
404
|
widened: boolean;
|
|
308
405
|
excursions: unknown[];
|
|
@@ -313,6 +410,7 @@ describe("search tools", () => {
|
|
|
313
410
|
|
|
314
411
|
test("lookup_booking reports the ticket the caller is actually holding", async () => {
|
|
315
412
|
const ctx = makeCtx();
|
|
413
|
+
await atDesk("flight", ctx);
|
|
316
414
|
await run("update_ticket", { flightId: "LX52" }, ctx);
|
|
317
415
|
await run("confirm_action", ctx);
|
|
318
416
|
const booking = (await run("lookup_booking", ctx)) as {
|
|
@@ -357,8 +455,10 @@ describe("tripView projection", () => {
|
|
|
357
455
|
|
|
358
456
|
test("totals every confirmed booking", async () => {
|
|
359
457
|
const ctx = makeCtx();
|
|
458
|
+
await atDesk("hotel", ctx);
|
|
360
459
|
await run("book_hotel", { hotelId: "H3", nights: 2 }, ctx); // 2 × 180
|
|
361
460
|
await run("confirm_action", ctx);
|
|
461
|
+
await atDesk("excursion", ctx);
|
|
362
462
|
await run("book_excursion", { excursionId: "E1" }, ctx); // 35
|
|
363
463
|
await run("confirm_action", ctx);
|
|
364
464
|
|
|
@@ -7,15 +7,24 @@ import { tripProjection } from "./shared.ts";
|
|
|
7
7
|
* two mechanisms worth reading for are the dialog stack (`routing.ts`) and the
|
|
8
8
|
* confirmation gate (`stageAction` / `confirm_action`).
|
|
9
9
|
*
|
|
10
|
-
* **Every tool in `tools/` belongs to this one agent
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
10
|
+
* **Every tool in `tools/` belongs to this one agent, and the delegation is
|
|
11
|
+
* ENFORCED anyway.** Their graph gives each specialist node its own bound tool
|
|
12
|
+
* set, so a specialist physically cannot call another desk's tools. A voice
|
|
13
|
+
* session has one model with one tool list for its whole life, so a list cannot
|
|
14
|
+
* be narrowed here — but a TOOL can refuse, which is the same guarantee by a
|
|
15
|
+
* route that works mid-call: every desk tool checks the stack first
|
|
16
|
+
* (`requireDesk` in `shared.ts`) and answers a `ToolFailure` naming the
|
|
17
|
+
* `to_…_assistant` to call. So the position the sidebar renders is always the
|
|
18
|
+
* position the work is being done at, and the specialist's brief has always
|
|
19
|
+
* been read before that desk's first search.
|
|
20
|
+
*
|
|
21
|
+
* This used to be asked for in the prompt instead, described here as the honest
|
|
22
|
+
* limit of the port. It was measured and the prompt lost — 0 of 5 live runs, the
|
|
23
|
+
* model searching hotels from the concierge desk every time — so the eval case
|
|
24
|
+
* that predicted it (`agent.eval.test.ts`, "a hotel request goes to the hotel
|
|
25
|
+
* desk") is what turned the narrowing into a mechanism. `complete_or_escalate`
|
|
26
|
+
* is still described the way it is for the other half of the reason: the stack
|
|
27
|
+
* is what the caller and the sidebar can both see.
|
|
19
28
|
*/
|
|
20
29
|
export default agent({
|
|
21
30
|
name: "Swiss Air Concierge",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* | `ToFlightBookingAssistant` & friends (delegation tools) | the four tools {@link SPECIALISTS} generates in `routing.ts` |
|
|
15
15
|
* | `CompleteOrEscalate` | `complete_or_escalate`, which pops the same stack |
|
|
16
16
|
* | `interrupt_before=["…_sensitive_tools"]` | {@link stageAction} + `confirm_action` |
|
|
17
|
+
* | a specialist node's BOUND tool set | {@link requireDesk}, checked by every desk tool |
|
|
17
18
|
* | `fetch_user_flight_information` (sqlite) | `lookup_booking` over {@link seedTrip} |
|
|
18
19
|
*
|
|
19
20
|
* **The dialog stack is what keeps a long call on the rails.** Their insight is
|
|
@@ -23,7 +24,9 @@
|
|
|
23
24
|
* session's prompt is fixed at connect — so the specialist's brief arrives as
|
|
24
25
|
* the DELEGATION TOOL'S RESULT, which is the last thing the model reads before
|
|
25
26
|
* it answers. The stack itself is real state either way, which is what
|
|
26
|
-
* `complete_or_escalate` pops and what the sidebar renders
|
|
27
|
+
* `complete_or_escalate` pops and what the sidebar renders — and what
|
|
28
|
+
* {@link requireDesk} makes binding, so the position is never merely a label on
|
|
29
|
+
* work that happened somewhere else.
|
|
27
30
|
*
|
|
28
31
|
* **`interrupt_before` becomes a spoken confirmation, and that is not a
|
|
29
32
|
* downgrade.** Their graph halts before a sensitive tool and waits for a human
|
|
@@ -207,6 +210,19 @@ export interface Specialist {
|
|
|
207
210
|
instructions: string;
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Each brief spells the STAGE-then-read-back ORDER out, and that is a fix
|
|
215
|
+
* measured rather than a preference.
|
|
216
|
+
*
|
|
217
|
+
* The flight desk's used to say "read the fare change back to the caller before
|
|
218
|
+
* you change a ticket", which is true of `confirm_action` and reads as a rule
|
|
219
|
+
* about `update_ticket` — and live, that is how it was read: the desk searched,
|
|
220
|
+
* quoted a fare it had not staged, waited for a yes, and then called
|
|
221
|
+
* `confirm_action` with nothing waiting for it. Two of three runs never staged
|
|
222
|
+
* anything at all. The sentence to read back IS the staging tool's return
|
|
223
|
+
* value, so a brief that puts the read-back first describes a call the desk
|
|
224
|
+
* cannot make.
|
|
225
|
+
*/
|
|
210
226
|
export const SPECIALISTS: Record<SpecialistId, Specialist> = {
|
|
211
227
|
flight: {
|
|
212
228
|
title: "flight desk",
|
|
@@ -214,9 +230,22 @@ export const SPECIALISTS: Record<SpecialistId, Specialist> = {
|
|
|
214
230
|
"You are now the flight desk. Handle searching and changing flights only.",
|
|
215
231
|
"Search before you quote anything, and be persistent: if the first search",
|
|
216
232
|
"finds nothing, widen it rather than telling the caller there is nothing.",
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
233
|
+
"update_ticket is a QUOTE, not a change: it moves nothing, so call it as",
|
|
234
|
+
"soon as you know which flight they want and never ask permission first.",
|
|
235
|
+
"Changing a ticket is TWO steps and never one: call update_ticket first —",
|
|
236
|
+
"it changes nothing and hands you the sentence to say — then read the",
|
|
237
|
+
"flight number, the day, the departure time and the fare change back to",
|
|
238
|
+
"the caller and ask them plainly to confirm. A ticket is only changed once",
|
|
239
|
+
"confirm_action has run, and confirm_action has nothing to apply until",
|
|
240
|
+
"update_ticket has staged it.",
|
|
241
|
+
"Never quote a departure time or a fare you did not just read out of a",
|
|
242
|
+
"tool result, and never end a turn on 'shall I confirm?' with nothing",
|
|
243
|
+
"staged: update_ticket goes out in the SAME turn as that question, before",
|
|
244
|
+
"you stop. There is nothing for a yes to apply until it has.",
|
|
245
|
+
"Your next action, right now, is a TOOL and not a sentence: call",
|
|
246
|
+
"update_ticket if you already know which flight they want, or",
|
|
247
|
+
"search_flights if you do not. Do not speak until one of them has",
|
|
248
|
+
"answered — anything you would say before that is invented.",
|
|
220
249
|
"If the caller wants a hotel, a car or something to do, or changes their",
|
|
221
250
|
"mind, call complete_or_escalate instead of improvising.",
|
|
222
251
|
].join(" "),
|
|
@@ -227,8 +256,18 @@ export const SPECIALISTS: Record<SpecialistId, Specialist> = {
|
|
|
227
256
|
"You are now the hotel desk. Handle searching and booking hotels only.",
|
|
228
257
|
"Ask which city and roughly what budget, then search. Offer at most three",
|
|
229
258
|
"options out loud, cheapest first, with the neighbourhood and nightly rate.",
|
|
230
|
-
"
|
|
231
|
-
"
|
|
259
|
+
"book_hotel is a QUOTE, not a change: it holds nothing, so call it as",
|
|
260
|
+
"soon as you know what the caller wants and never ask permission",
|
|
261
|
+
"first.",
|
|
262
|
+
"Booking is TWO steps and never one: call book_hotel first — it holds",
|
|
263
|
+
"nothing and hands you the sentence to say — then read the hotel, the",
|
|
264
|
+
"number of nights and the total back and ask them to confirm. A room is",
|
|
265
|
+
"only held once confirm_action has run, and confirm_action has nothing to",
|
|
266
|
+
"apply until book_hotel has staged it.",
|
|
267
|
+
"Your next action, right now, is a TOOL and not a sentence: call",
|
|
268
|
+
"book_hotel if you already know which room and how many nights, or",
|
|
269
|
+
"search_hotels if you do not. Do not speak until one of them has",
|
|
270
|
+
"answered — anything you would say before that is invented.",
|
|
232
271
|
"If the caller wants flights, a car or an excursion, call",
|
|
233
272
|
"complete_or_escalate.",
|
|
234
273
|
].join(" "),
|
|
@@ -238,8 +277,19 @@ export const SPECIALISTS: Record<SpecialistId, Specialist> = {
|
|
|
238
277
|
instructions: [
|
|
239
278
|
"You are now the car rental desk. Handle searching and booking cars only.",
|
|
240
279
|
"Ask the city and how many days, then search. Name the vendor, the tier",
|
|
241
|
-
"and the daily rate
|
|
242
|
-
"
|
|
280
|
+
"and the daily rate.",
|
|
281
|
+
"book_car_rental is a QUOTE, not a change: it holds nothing, so call it as",
|
|
282
|
+
"soon as you know what the caller wants and never ask permission",
|
|
283
|
+
"first.",
|
|
284
|
+
"Reserving is TWO steps and never one: call book_car_rental first — it",
|
|
285
|
+
"reserves nothing and hands you the sentence to say — then read the total",
|
|
286
|
+
"back and ask them to confirm. A car is only reserved once confirm_action",
|
|
287
|
+
"has run, and confirm_action has nothing to apply until book_car_rental",
|
|
288
|
+
"has staged it.",
|
|
289
|
+
"Your next action, right now, is a TOOL and not a sentence: call",
|
|
290
|
+
"book_car_rental if you already know which car and how many days, or",
|
|
291
|
+
"search_car_rentals if you do not. Do not speak until one of them has",
|
|
292
|
+
"answered — anything you would say before that is invented.",
|
|
243
293
|
"If the caller wants flights, a hotel or an excursion, call",
|
|
244
294
|
"complete_or_escalate.",
|
|
245
295
|
].join(" "),
|
|
@@ -251,7 +301,17 @@ export const SPECIALISTS: Record<SpecialistId, Specialist> = {
|
|
|
251
301
|
"to do only. Ask the city and what kind of thing they enjoy, then search.",
|
|
252
302
|
"Describe two or three options in a sentence each — this is the part of",
|
|
253
303
|
"the call a caller actually wants to hear about.",
|
|
254
|
-
"
|
|
304
|
+
"book_excursion is a QUOTE, not a change: it holds nothing, so call it as",
|
|
305
|
+
"soon as you know what the caller wants and never ask permission",
|
|
306
|
+
"first.",
|
|
307
|
+
"Booking is TWO steps and never one: call book_excursion first — it books",
|
|
308
|
+
"nothing and hands you the sentence to say — then read it back and ask",
|
|
309
|
+
"them to confirm. A booking is only made once confirm_action has run, and",
|
|
310
|
+
"confirm_action has nothing to apply until book_excursion has staged it.",
|
|
311
|
+
"Your next action, right now, is a TOOL and not a sentence: call",
|
|
312
|
+
"book_excursion if you already know which one they want, or",
|
|
313
|
+
"search_excursions if you do not. Do not speak until one of them has",
|
|
314
|
+
"answered — anything you would say before that is invented.",
|
|
255
315
|
"If the caller wants flights, a hotel or a car, call complete_or_escalate.",
|
|
256
316
|
].join(" "),
|
|
257
317
|
},
|
|
@@ -347,6 +407,47 @@ export function note(state: TripState, entry: string): void {
|
|
|
347
407
|
pushCapped(state.log, entry, MAX_LOG_ENTRIES);
|
|
348
408
|
}
|
|
349
409
|
|
|
410
|
+
// ─── The desk gate ───────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Is the call at `id`'s desk? A {@link ToolFailure} naming the way in if not.
|
|
414
|
+
*
|
|
415
|
+
* **This is the narrowing their graph gets for free, and it used to be prose.**
|
|
416
|
+
* Each specialist node there binds its own tool set, so a node physically cannot
|
|
417
|
+
* call another desk's tools; a voice session has ONE model with ONE tool list
|
|
418
|
+
* for its whole life, so for a long time the stack was real state that the model
|
|
419
|
+
* was merely ASKED to respect — `agent.ts` said so in place, and this guide's
|
|
420
|
+
* own eval predicted the failure. Measured: the prompt lost 0 of 5 live runs.
|
|
421
|
+
* The caller said "I need a hotel in Boston" and the model called
|
|
422
|
+
* `search_hotels` at the concierge desk every time, so `dialogState` said
|
|
423
|
+
* `primary` while the call was plainly doing hotel work — a stack the sidebar
|
|
424
|
+
* renders and nothing keeps true.
|
|
425
|
+
*
|
|
426
|
+
* A tool list cannot be narrowed mid-session, but a tool can REFUSE, and a
|
|
427
|
+
* refusal is a thing the model recovers from inside the same turn: it reads the
|
|
428
|
+
* failure, calls `to_hotel_assistant`, gets the desk's brief, and searches. So
|
|
429
|
+
* the delegation is now structural — every desk tool is reachable only from its
|
|
430
|
+
* own desk — and the brief the delegation returns is guaranteed to have been
|
|
431
|
+
* read before any of that desk's work happens, which was the whole point of
|
|
432
|
+
* handing it back as a tool result.
|
|
433
|
+
*
|
|
434
|
+
* It costs one wasted round trip the first time the model reaches past the
|
|
435
|
+
* stack, which is the right price: the alternative is a desk whose position is
|
|
436
|
+
* decoration.
|
|
437
|
+
*/
|
|
438
|
+
export function requireDesk(state: FrozenTripState, id: SpecialistId): ToolFailure | undefined {
|
|
439
|
+
const at = activeAssistant(state);
|
|
440
|
+
if (at === id) return undefined;
|
|
441
|
+
const here = at === "primary" ? "the main concierge" : SPECIALISTS[at].title;
|
|
442
|
+
return {
|
|
443
|
+
error:
|
|
444
|
+
`That tool belongs to the ${SPECIALISTS[id].title} and this call is at ${here}. ` +
|
|
445
|
+
`Nothing was searched, staged or booked. Call to_${id}_assistant with what the ` +
|
|
446
|
+
"caller asked for — its answer is that desk's brief — and then call this tool again. " +
|
|
447
|
+
"Do not tell the caller about any of this; they should hear one continuous conversation.",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
350
451
|
// ─── The confirmation gate ───────────────────────────────────────────────────
|
|
351
452
|
|
|
352
453
|
/**
|
|
@@ -10,15 +10,21 @@ You do not do the specialist work yourself. There are four desks, and each one h
|
|
|
10
10
|
How to run a call:
|
|
11
11
|
|
|
12
12
|
- Start with `lookup_booking` so you know who you are speaking to and what they are holding.
|
|
13
|
-
-
|
|
13
|
+
- **A desk's tools only work while the call is at that desk, and they refuse from anywhere else.** So the moment the caller raises something a desk owns — flights, hotels, cars, excursions — your very next tool call is that desk's `to_…_assistant`, passing along what they asked for. Never a search or a booking first: at the concierge desk those refuse, and you have spent the caller's time on nothing.
|
|
14
|
+
- Do not mention the transfer — the caller should hear one continuous conversation.
|
|
14
15
|
- The desk's reply tells you how it works. Follow it until it is done, then use `complete_or_escalate` to come back.
|
|
15
16
|
- Answer small talk, general questions and anything about the existing booking yourself.
|
|
16
17
|
|
|
17
18
|
Confirming changes — this part matters:
|
|
18
19
|
|
|
19
20
|
- Every tool that would change a booking stages the change instead of making it. Nothing has happened until `confirm_action` runs.
|
|
21
|
+
- **Staging is a QUOTE, not a change, so it needs no permission.** `update_ticket`, `book_hotel`, `book_car_rental` and `book_excursion` price the request and hand you the sentence to say; they move nothing. Call the staging tool as soon as you know what the caller wants — you are getting them a quote, not doing the thing — and never ask whether you may.
|
|
22
|
+
- **So the staging tool comes before the question, always.** "Shall I confirm?" is a question about a staged change, so the tool that stages it runs first and its answer is what you read back. Asking before staging is asking about something that does not exist, and the yes it earns has nothing to apply.
|
|
23
|
+
- **Never end a turn on "shall I confirm?" with nothing staged.** The staging call and that question belong to the same turn: stage, then ask, in that order, before you stop. If you have already asked and the caller says anything other than no, stage it now — asking a second time does not stage it either, and a caller who has to repeat themselves is a caller nobody is helping.
|
|
24
|
+
- **Never say a time, a fare or a total that did not come out of a tool result on this call**, and never say you are doing something — "I'll move your ticket", "I'm booking that" — before the tool that stages it has answered. If you need a number you have not got, call the desk's search first.
|
|
20
25
|
- When a tool answers with `awaitingConfirmation`, read the change back in your own words and ask a yes-or-no question. Say the price.
|
|
21
26
|
- If the caller says yes, call `confirm_action`. If they say no, or want different details, call `cancel_action`.
|
|
27
|
+
- **`confirm_action` only ever answers a read-back you have just given.** If no tool has answered `awaitingConfirmation` on this call, there is nothing staged, `confirm_action` will refuse, and the caller's request is still waiting to be done — reach for the tool that stages it instead. A caller who says "yes" or "go ahead" before anything is staged is agreeing to the request they already made, not to a read-back.
|
|
22
28
|
- Never call `confirm_action` on your own initiative, and never claim something is booked before it has run.
|
|
23
29
|
|
|
24
30
|
Voice manners: numbers spoken out ("six forty" not "$640.00"), flight numbers letter by letter when reading them back, and one question at a time.
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { stageAction, tripSlot } from "../shared.ts";
|
|
2
|
+
import { requireDesk, stageAction, tripSlot } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
/** SENSITIVE — their `book_car_rental`, staged rather than applied. */
|
|
5
5
|
export default tripSlot.updateTool({
|
|
6
6
|
description:
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"The CAR RENTAL DESK's booking tool: reserve a rental car. Only usable while the call " +
|
|
8
|
+
"is at that desk. This does NOT reserve anything yet — it stages the reservation so " +
|
|
9
|
+
"you can read the total back and hear a yes.",
|
|
9
10
|
inputSchema: z.object({
|
|
10
11
|
carId: z.string().max(20).describe("The car id from search_car_rentals, e.g. 'C2'"),
|
|
11
12
|
days: z.number().int().min(1).max(60).describe("How many days"),
|
|
12
13
|
}),
|
|
13
14
|
execute(args, trip, ctx) {
|
|
15
|
+
const offDesk = requireDesk(trip, "car_rental");
|
|
16
|
+
if (offDesk) return offDesk;
|
|
14
17
|
return stageAction(ctx, trip, {
|
|
15
18
|
kind: "book_car",
|
|
16
19
|
carId: args.carId.toUpperCase(),
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { stageAction, tripSlot } from "../shared.ts";
|
|
2
|
+
import { requireDesk, stageAction, tripSlot } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
/** SENSITIVE — their `book_excursion`, staged rather than applied. */
|
|
5
5
|
export default tripSlot.updateTool({
|
|
6
6
|
description:
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"The EXCURSIONS DESK's booking tool: book an excursion. Only usable while the call is " +
|
|
8
|
+
"at that desk. This does NOT book anything yet — it stages the booking so you can read " +
|
|
9
|
+
"it back and hear a yes.",
|
|
9
10
|
inputSchema: z.object({
|
|
10
11
|
excursionId: z.string().max(20).describe("The excursion id from search_excursions, e.g. 'E2'"),
|
|
11
12
|
}),
|
|
12
13
|
execute(args, trip, ctx) {
|
|
14
|
+
const offDesk = requireDesk(trip, "excursion");
|
|
15
|
+
if (offDesk) return offDesk;
|
|
13
16
|
return stageAction(ctx, trip, {
|
|
14
17
|
kind: "book_excursion",
|
|
15
18
|
excursionId: args.excursionId.toUpperCase(),
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { stageAction, tripSlot } from "../shared.ts";
|
|
2
|
+
import { requireDesk, stageAction, tripSlot } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
/** SENSITIVE — their `book_hotel`, staged rather than applied. */
|
|
5
5
|
export default tripSlot.updateTool({
|
|
6
6
|
description:
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"The HOTEL DESK's booking tool: hold a hotel room. Only usable while the call is at " +
|
|
8
|
+
"that desk. This does NOT book anything yet — it stages the booking so you can read " +
|
|
9
|
+
"the total back and hear a yes.",
|
|
9
10
|
inputSchema: z.object({
|
|
10
11
|
hotelId: z.string().max(20).describe("The hotel id from search_hotels, e.g. 'H1'"),
|
|
11
12
|
nights: z.number().int().min(1).max(30).describe("How many nights"),
|
|
12
13
|
}),
|
|
13
14
|
execute(args, trip, ctx) {
|
|
15
|
+
const offDesk = requireDesk(trip, "hotel");
|
|
16
|
+
if (offDesk) return offDesk;
|
|
14
17
|
return stageAction(ctx, trip, {
|
|
15
18
|
kind: "book_hotel",
|
|
16
19
|
hotelId: args.hotelId.toUpperCase(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stageAction, tripSlot } from "../shared.ts";
|
|
1
|
+
import { requireDesk, stageAction, tripSlot } from "../shared.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SENSITIVE — their `cancel_ticket`. The most destructive thing on the call and
|
|
@@ -7,9 +7,12 @@ import { stageAction, tripSlot } from "../shared.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export default tripSlot.updateTool({
|
|
9
9
|
description:
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"The FLIGHT DESK's cancellation tool: cancel the caller's ticket outright. Only usable " +
|
|
11
|
+
"while the call is at that desk. This does NOT cancel anything yet — it stages the " +
|
|
12
|
+
"cancellation so you can read it back and hear a yes.",
|
|
12
13
|
execute(_args, trip, ctx) {
|
|
14
|
+
const offDesk = requireDesk(trip, "flight");
|
|
15
|
+
if (offDesk) return offDesk;
|
|
13
16
|
if (!trip.ticket) return { error: "There is no ticket to cancel." };
|
|
14
17
|
return stageAction(ctx, trip, { kind: "cancel_ticket" });
|
|
15
18
|
},
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import { CAR_RENTALS, formatPrice } from "../shared.ts";
|
|
2
|
+
import { CAR_RENTALS, formatPrice, requireDesk, tripSlot } from "../shared.ts";
|
|
4
3
|
|
|
5
4
|
/** Their `search_car_rentals`, by city and tier. */
|
|
6
|
-
export default tool({
|
|
7
|
-
description:
|
|
5
|
+
export default tripSlot.tool({
|
|
6
|
+
description:
|
|
7
|
+
"The CAR RENTAL DESK's search: cars by city, and optionally by tier (compact, midsize, " +
|
|
8
|
+
"suv). Only usable while the call is at that desk — from anywhere else it refuses, so " +
|
|
9
|
+
"call to_car_rental_assistant first.",
|
|
8
10
|
inputSchema: z.object({
|
|
9
11
|
city: z.string().max(80).describe("City to search, e.g. 'Boston'"),
|
|
10
12
|
tier: z.string().max(40).describe("compact, midsize or suv").optional(),
|
|
11
13
|
}),
|
|
12
|
-
execute(args) {
|
|
14
|
+
execute(args, trip) {
|
|
15
|
+
const offDesk = requireDesk(trip, "car_rental");
|
|
16
|
+
if (offDesk) return offDesk;
|
|
13
17
|
const city = args.city.trim().toLowerCase();
|
|
14
18
|
const tier = args.tier?.trim().toLowerCase();
|
|
15
19
|
const cars = CAR_RENTALS.filter(
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import { EXCURSIONS, formatPrice } from "../shared.ts";
|
|
2
|
+
import { EXCURSIONS, formatPrice, requireDesk, tripSlot } from "../shared.ts";
|
|
4
3
|
|
|
5
4
|
/** Their `search_trip_recommendations` — city plus a loose keyword, matched
|
|
6
5
|
* against the name and the kind so "boat" and "sail" both land. */
|
|
7
|
-
export default tool({
|
|
8
|
-
description:
|
|
6
|
+
export default tripSlot.tool({
|
|
7
|
+
description:
|
|
8
|
+
"The EXCURSIONS DESK's search: things to do in a city, with an optional keyword matched " +
|
|
9
|
+
"loosely. Only usable while the call is at that desk — from anywhere else it refuses, so " +
|
|
10
|
+
"call to_excursion_assistant first.",
|
|
9
11
|
inputSchema: z.object({
|
|
10
12
|
city: z.string().max(80).describe("City to search, e.g. 'Boston'"),
|
|
11
13
|
keyword: z
|
|
@@ -14,7 +16,9 @@ export default tool({
|
|
|
14
16
|
.describe("What they enjoy, e.g. 'food', 'boat', 'history'")
|
|
15
17
|
.optional(),
|
|
16
18
|
}),
|
|
17
|
-
execute(args) {
|
|
19
|
+
execute(args, trip) {
|
|
20
|
+
const offDesk = requireDesk(trip, "excursion");
|
|
21
|
+
if (offDesk) return offDesk;
|
|
18
22
|
const city = args.city.trim().toLowerCase();
|
|
19
23
|
const keyword = args.keyword?.trim().toLowerCase();
|
|
20
24
|
const inCity = EXCURSIONS.filter((e) => e.city.toLowerCase().includes(city));
|