@alexkroman1/aai-cli 6.11.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/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- 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/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- 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/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- 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/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- 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/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- 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 +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- 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/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- 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 +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- 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 +150 -7
- 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 +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- 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 +8 -5
- 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-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
ok,
|
|
7
|
+
okPosition,
|
|
8
|
+
toolRunner,
|
|
9
|
+
withDiscoveredTools,
|
|
10
|
+
} from "@alexkroman1/aai/testing";
|
|
5
11
|
import { describe, expect, test } from "vitest";
|
|
6
12
|
import authoredAgent from "./agent.ts";
|
|
7
13
|
|
|
@@ -18,7 +24,15 @@ const agentDef = withDiscoveredTools(
|
|
|
18
24
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
19
25
|
);
|
|
20
26
|
|
|
21
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
activeAssistant,
|
|
29
|
+
FLIGHTS,
|
|
30
|
+
gateFlow,
|
|
31
|
+
type SpecialistId,
|
|
32
|
+
tripProjection,
|
|
33
|
+
tripSlot,
|
|
34
|
+
tripView,
|
|
35
|
+
} from "./shared.ts";
|
|
22
36
|
|
|
23
37
|
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
24
38
|
|
|
@@ -26,17 +40,31 @@ import { activeAssistant, FLIGHTS, tripProjection, tripSlot, tripView } from "./
|
|
|
26
40
|
* below rests on. */
|
|
27
41
|
const makeCtx = (): ToolContext => createToolContext();
|
|
28
42
|
|
|
29
|
-
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
30
|
-
*
|
|
31
|
-
* what is local is only which
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup,
|
|
44
|
+
* its "no such tool" message and the args-or-context shape are all
|
|
45
|
+
* `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
|
|
46
|
+
* agent it runs against. */
|
|
47
|
+
const run = toolRunner(agentDef);
|
|
34
48
|
|
|
35
49
|
/** The state a tool just mutated, read back the way `syncState` reads it. */
|
|
36
50
|
function stateOf(ctx: ToolContext) {
|
|
37
51
|
return tripSlot.get(ctx);
|
|
38
52
|
}
|
|
39
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
|
+
|
|
40
68
|
// ─── 1. The dialog stack ─────────────────────────────────────────────────────
|
|
41
69
|
|
|
42
70
|
describe("dialog stack (routing.ts)", () => {
|
|
@@ -85,6 +113,72 @@ describe("dialog stack (routing.ts)", () => {
|
|
|
85
113
|
});
|
|
86
114
|
});
|
|
87
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
|
+
|
|
88
182
|
// ─── 2. The confirmation gate ────────────────────────────────────────────────
|
|
89
183
|
//
|
|
90
184
|
// Their graph halts before a sensitive tool (`interrupt_before`) and resumes on
|
|
@@ -94,13 +188,14 @@ describe("dialog stack (routing.ts)", () => {
|
|
|
94
188
|
|
|
95
189
|
describe("sensitive tools stage rather than act", () => {
|
|
96
190
|
test.each([
|
|
97
|
-
["update_ticket", { flightId: "LX52" }],
|
|
98
|
-
["cancel_ticket", {}],
|
|
99
|
-
["book_hotel", { hotelId: "H1", nights: 2 }],
|
|
100
|
-
["book_car_rental", { carId: "C2", days: 3 }],
|
|
101
|
-
["book_excursion", { excursionId: "E2" }],
|
|
102
|
-
])("%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) => {
|
|
103
197
|
const ctx = makeCtx();
|
|
198
|
+
await atDesk(desk, ctx);
|
|
104
199
|
const before = structuredClone(stateOf(ctx));
|
|
105
200
|
|
|
106
201
|
const staged = (await run(name, args, ctx)) as { awaitingConfirmation: boolean };
|
|
@@ -114,38 +209,51 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
114
209
|
|
|
115
210
|
test("confirm_action applies the staged change, and only then", async () => {
|
|
116
211
|
const ctx = makeCtx();
|
|
212
|
+
await atDesk("hotel", ctx);
|
|
117
213
|
await run("book_hotel", { hotelId: "H1", nights: 3 }, ctx);
|
|
118
214
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
215
|
+
// Staging moved the gate, in the same window it wrote `pending`.
|
|
216
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
217
|
+
|
|
218
|
+
// A gated tool answers the flow's POSITION wrapped around the body's own
|
|
219
|
+
// return value, so the applied sentence is under `result`. `okPosition`
|
|
220
|
+
// keeps the position and THROWS on a refusal, quoting it — where the cast
|
|
221
|
+
// it replaces read `undefined` off the failure and died three assertions
|
|
222
|
+
// later on a property of undefined.
|
|
223
|
+
const confirmed = okPosition<{ applied: string; reference: string }>(
|
|
224
|
+
await run("confirm_action", ctx),
|
|
225
|
+
);
|
|
226
|
+
expect(confirmed.result.applied).toContain("Harborview Suites");
|
|
124
227
|
// 3 nights at $265.
|
|
125
|
-
expect(
|
|
228
|
+
expect(confirmed.result.applied).toContain("$795");
|
|
229
|
+
// SETTLED: the gate re-armed as part of the same call.
|
|
230
|
+
expect(confirmed.state).toBe("browsing");
|
|
126
231
|
|
|
127
232
|
const state = stateOf(ctx);
|
|
128
233
|
expect(state.pending).toBeNull();
|
|
129
234
|
expect(state.bookings).toHaveLength(1);
|
|
130
|
-
expect(state.bookings[0]?.reference).toBe(
|
|
235
|
+
expect(state.bookings[0]?.reference).toBe(confirmed.result.reference);
|
|
131
236
|
|
|
132
|
-
// The gate re-arms
|
|
133
|
-
|
|
134
|
-
|
|
237
|
+
// The gate re-arms, and now REFUSES rather than reporting an empty apply —
|
|
238
|
+
// `when: "awaitingConfirmation"` is what a second confirm meets.
|
|
239
|
+
expect(await run("confirm_action", ctx)).toMatchObject({
|
|
240
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
135
241
|
});
|
|
136
242
|
});
|
|
137
243
|
|
|
138
244
|
test("cancel_action drops the staged change and leaves the booking alone", async () => {
|
|
139
245
|
const ctx = makeCtx();
|
|
246
|
+
await atDesk("flight", ctx);
|
|
140
247
|
await run("update_ticket", { flightId: "LX54" }, ctx);
|
|
141
|
-
const dropped = (await run("cancel_action",
|
|
142
|
-
expect(dropped.discarded).toContain("LX54");
|
|
248
|
+
const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
|
|
249
|
+
expect(dropped.result.discarded).toContain("LX54");
|
|
250
|
+
expect(dropped.state).toBe("browsing");
|
|
143
251
|
|
|
144
252
|
const state = stateOf(ctx);
|
|
145
253
|
expect(state.pending).toBeNull();
|
|
146
254
|
expect(state.ticket?.flightId).toBe("LX40");
|
|
147
|
-
expect(await run("cancel_action",
|
|
148
|
-
|
|
255
|
+
expect(await run("cancel_action", ctx)).toMatchObject({
|
|
256
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
149
257
|
});
|
|
150
258
|
});
|
|
151
259
|
|
|
@@ -153,6 +261,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
153
261
|
// Refused where the model can still recover — before the caller is asked to
|
|
154
262
|
// confirm a flight the airline does not fly.
|
|
155
263
|
const ctx = makeCtx();
|
|
264
|
+
await atDesk("flight", ctx);
|
|
156
265
|
expect(await run("update_ticket", { flightId: "ZZ99" }, ctx)).toEqual({
|
|
157
266
|
error: "No flight ZZ99 in the schedule.",
|
|
158
267
|
});
|
|
@@ -161,29 +270,35 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
161
270
|
|
|
162
271
|
test("cancelling the ticket makes a later ticket change impossible", async () => {
|
|
163
272
|
const ctx = makeCtx();
|
|
164
|
-
await
|
|
165
|
-
await run("
|
|
273
|
+
await atDesk("flight", ctx);
|
|
274
|
+
await run("cancel_ticket", ctx);
|
|
275
|
+
await run("confirm_action", ctx);
|
|
166
276
|
expect(stateOf(ctx).ticket).toBeNull();
|
|
167
277
|
|
|
168
278
|
expect(await run("update_ticket", { flightId: "LX52" }, ctx)).toEqual({
|
|
169
279
|
error: "This caller has no ticket to move — it was cancelled on this call.",
|
|
170
280
|
});
|
|
171
|
-
expect(await run("cancel_ticket",
|
|
281
|
+
expect(await run("cancel_ticket", ctx)).toEqual({ error: "There is no ticket to cancel." });
|
|
172
282
|
});
|
|
173
283
|
|
|
174
284
|
test("a second staging is REFUSED rather than overwriting the first", async () => {
|
|
175
285
|
// The LLM loop runs a step's tool calls concurrently, so two sensitive
|
|
176
|
-
// tools in one step is ordinary: the model hears "move my flight and
|
|
177
|
-
// 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
|
|
178
288
|
// second win — both answered `awaitingConfirmation`, the caller said yes
|
|
179
289
|
// once, and one of the two changes was silently dropped forever.
|
|
180
290
|
const ctx = makeCtx();
|
|
291
|
+
await atDesk("flight", ctx);
|
|
181
292
|
const first = (await run("update_ticket", { flightId: "LX52" }, ctx)) as {
|
|
182
293
|
awaitingConfirmation: boolean;
|
|
183
294
|
};
|
|
184
295
|
expect(first.awaitingConfirmation).toBe(true);
|
|
185
296
|
|
|
186
|
-
|
|
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 {
|
|
187
302
|
error: string;
|
|
188
303
|
};
|
|
189
304
|
// The refusal NAMES what is already waiting, which is what lets the model
|
|
@@ -193,11 +308,17 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
193
308
|
|
|
194
309
|
// The first staging is untouched, and it is what a yes applies.
|
|
195
310
|
expect(stateOf(ctx).pending).toEqual({ kind: "update_ticket", flightId: "LX52" });
|
|
196
|
-
|
|
311
|
+
// A refused SECOND staging must not have moved the gate either — it was
|
|
312
|
+
// already `awaitingConfirmation` and the refusal changed nothing.
|
|
313
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
314
|
+
// `ok` is `okPosition` with `.result` taken off: this assertion is about
|
|
315
|
+
// what the apply DID, not about where the gate landed.
|
|
316
|
+
const applied = ok<{ applied: string }>(await run("confirm_action", ctx));
|
|
197
317
|
expect(applied.applied).toContain("LX52");
|
|
198
318
|
expect(stateOf(ctx).bookings).toEqual([]);
|
|
199
319
|
|
|
200
|
-
// 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);
|
|
201
322
|
const retried = (await run("book_hotel", { hotelId: "H1", nights: 2 }, ctx)) as {
|
|
202
323
|
awaitingConfirmation: boolean;
|
|
203
324
|
};
|
|
@@ -206,8 +327,10 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
206
327
|
|
|
207
328
|
test("cancel_action clears the block, so a declined change does not wedge the desk", async () => {
|
|
208
329
|
const ctx = makeCtx();
|
|
330
|
+
await atDesk("car_rental", ctx);
|
|
209
331
|
await run("book_car_rental", { carId: "C2", days: 3 }, ctx);
|
|
210
|
-
await run("cancel_action",
|
|
332
|
+
await run("cancel_action", ctx);
|
|
333
|
+
await atDesk("excursion", ctx);
|
|
211
334
|
const staged = (await run("book_excursion", { excursionId: "E2" }, ctx)) as {
|
|
212
335
|
awaitingConfirmation: boolean;
|
|
213
336
|
};
|
|
@@ -225,12 +348,14 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
225
348
|
|
|
226
349
|
await run("to_hotel_assistant", { request: "a room" }, first);
|
|
227
350
|
await run("book_hotel", { hotelId: "H3", nights: 1 }, first);
|
|
228
|
-
await run("confirm_action",
|
|
351
|
+
await run("confirm_action", first);
|
|
229
352
|
|
|
230
353
|
expect(activeAssistant(stateOf(second))).toBe("primary");
|
|
231
354
|
expect(stateOf(second).bookings).toEqual([]);
|
|
232
|
-
|
|
233
|
-
|
|
355
|
+
// The GATE is per-session too, not just the trip.
|
|
356
|
+
expect(gateFlow.position(second).state).toBe("browsing");
|
|
357
|
+
expect(await run("confirm_action", second)).toMatchObject({
|
|
358
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
234
359
|
});
|
|
235
360
|
expect(stateOf(first).bookings).toHaveLength(1);
|
|
236
361
|
});
|
|
@@ -241,6 +366,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
241
366
|
describe("search tools", () => {
|
|
242
367
|
test("an unmatched route widens to the whole schedule rather than answering nothing", async () => {
|
|
243
368
|
const ctx = makeCtx();
|
|
369
|
+
await atDesk("flight", ctx);
|
|
244
370
|
const hit = (await run("search_flights", { route: "Zurich to Boston" }, ctx)) as {
|
|
245
371
|
widened: boolean;
|
|
246
372
|
flights: unknown[];
|
|
@@ -258,6 +384,7 @@ describe("search tools", () => {
|
|
|
258
384
|
|
|
259
385
|
test("hotels come back cheapest first, and a ceiling filters them", async () => {
|
|
260
386
|
const ctx = makeCtx();
|
|
387
|
+
await atDesk("hotel", ctx);
|
|
261
388
|
const all = (await run("search_hotels", { city: "Boston" }, ctx)) as {
|
|
262
389
|
hotels: { perNight: string }[];
|
|
263
390
|
};
|
|
@@ -272,6 +399,7 @@ describe("search tools", () => {
|
|
|
272
399
|
|
|
273
400
|
test("an excursion keyword that matches nothing falls back to the city", async () => {
|
|
274
401
|
const ctx = makeCtx();
|
|
402
|
+
await atDesk("excursion", ctx);
|
|
275
403
|
const result = (await run("search_excursions", { city: "Boston", keyword: "skiing" }, ctx)) as {
|
|
276
404
|
widened: boolean;
|
|
277
405
|
excursions: unknown[];
|
|
@@ -282,9 +410,10 @@ describe("search tools", () => {
|
|
|
282
410
|
|
|
283
411
|
test("lookup_booking reports the ticket the caller is actually holding", async () => {
|
|
284
412
|
const ctx = makeCtx();
|
|
413
|
+
await atDesk("flight", ctx);
|
|
285
414
|
await run("update_ticket", { flightId: "LX52" }, ctx);
|
|
286
|
-
await run("confirm_action",
|
|
287
|
-
const booking = (await run("lookup_booking",
|
|
415
|
+
await run("confirm_action", ctx);
|
|
416
|
+
const booking = (await run("lookup_booking", ctx)) as {
|
|
288
417
|
passenger: string;
|
|
289
418
|
ticket: { flight: string; departs: string } | null;
|
|
290
419
|
};
|
|
@@ -326,10 +455,12 @@ describe("tripView projection", () => {
|
|
|
326
455
|
|
|
327
456
|
test("totals every confirmed booking", async () => {
|
|
328
457
|
const ctx = makeCtx();
|
|
458
|
+
await atDesk("hotel", ctx);
|
|
329
459
|
await run("book_hotel", { hotelId: "H3", nights: 2 }, ctx); // 2 × 180
|
|
330
|
-
await run("confirm_action",
|
|
460
|
+
await run("confirm_action", ctx);
|
|
461
|
+
await atDesk("excursion", ctx);
|
|
331
462
|
await run("book_excursion", { excursionId: "E1" }, ctx); // 35
|
|
332
|
-
await run("confirm_action",
|
|
463
|
+
await run("confirm_action", ctx);
|
|
333
464
|
|
|
334
465
|
const view = tripView(stateOf(ctx));
|
|
335
466
|
expect(view.total).toBe(395);
|
|
@@ -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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll, client, useAgentState
|
|
2
|
+
import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import type { TripView } from "./shared.ts";
|
|
4
4
|
import { SPECIALIST_IDS, SPECIALISTS, tripProjection } from "./shared.ts";
|
|
5
5
|
|
|
@@ -15,7 +15,6 @@ function deskLabel(id: (typeof DESKS)[number]): string {
|
|
|
15
15
|
* the transcript — the whole delegation mechanism is otherwise invisible.
|
|
16
16
|
*/
|
|
17
17
|
function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
18
|
-
const theme = useTheme();
|
|
19
18
|
return (
|
|
20
19
|
<div className="flex flex-wrap gap-1.5">
|
|
21
20
|
{DESKS.map((id) => {
|
|
@@ -23,12 +22,9 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
23
22
|
return (
|
|
24
23
|
<span
|
|
25
24
|
key={id}
|
|
26
|
-
className=
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
color: on ? theme.bg : theme.text,
|
|
30
|
-
opacity: on ? 1 : 0.55,
|
|
31
|
-
}}
|
|
25
|
+
className={`rounded-full px-2.5 py-1 text-[11px] font-medium capitalize ${
|
|
26
|
+
on ? "bg-aai-primary text-aai-bg" : "bg-aai-surface text-aai-text opacity-55"
|
|
27
|
+
}`}
|
|
32
28
|
>
|
|
33
29
|
{deskLabel(id)}
|
|
34
30
|
</span>
|
|
@@ -39,11 +35,10 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
function ItinerarySidebar() {
|
|
42
|
-
const theme = useTheme();
|
|
43
38
|
const trip = useAgentState(tripProjection);
|
|
44
39
|
|
|
45
40
|
return (
|
|
46
|
-
<div className="flex flex-col gap-4 p-4 h-full min-h-0
|
|
41
|
+
<div className="flex flex-col gap-4 p-4 h-full min-h-0 text-aai-text">
|
|
47
42
|
<div className="flex flex-col gap-2">
|
|
48
43
|
<h3 className="text-sm font-bold opacity-60 uppercase tracking-wide">{trip.passenger}</h3>
|
|
49
44
|
<DeskStrip active={trip.assistant} />
|
|
@@ -52,10 +47,7 @@ function ItinerarySidebar() {
|
|
|
52
47
|
{/* The staged action — the browser half of the confirmation gate. What is
|
|
53
48
|
on screen is exactly what the concierge just asked out loud. */}
|
|
54
49
|
{trip.pending && (
|
|
55
|
-
<div
|
|
56
|
-
className="rounded-lg p-3 text-sm"
|
|
57
|
-
style={{ background: theme.surface, border: `1px solid ${theme.primary}` }}
|
|
58
|
-
>
|
|
50
|
+
<div className="rounded-lg p-3 text-sm bg-aai-surface border border-aai-primary">
|
|
59
51
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
|
|
60
52
|
Waiting on your yes
|
|
61
53
|
</p>
|
|
@@ -66,7 +58,7 @@ function ItinerarySidebar() {
|
|
|
66
58
|
<div className="flex flex-col gap-2">
|
|
67
59
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Ticket</p>
|
|
68
60
|
{trip.ticket ? (
|
|
69
|
-
<div className="rounded-lg p-3
|
|
61
|
+
<div className="rounded-lg p-3 bg-aai-surface">
|
|
70
62
|
<p className="text-sm font-medium">
|
|
71
63
|
{trip.ticket.flightId} · {trip.ticket.route}
|
|
72
64
|
</p>
|
|
@@ -85,24 +77,20 @@ function ItinerarySidebar() {
|
|
|
85
77
|
{trip.bookings.map((booking) => (
|
|
86
78
|
<div
|
|
87
79
|
key={booking.reference}
|
|
88
|
-
className="flex items-center justify-between gap-3 rounded-lg p-3"
|
|
89
|
-
style={{ background: theme.surface }}
|
|
80
|
+
className="flex items-center justify-between gap-3 rounded-lg p-3 bg-aai-surface"
|
|
90
81
|
>
|
|
91
82
|
<div className="min-w-0">
|
|
92
83
|
<p className="truncate text-sm">{booking.summary}</p>
|
|
93
84
|
<p className="text-xs opacity-60">{booking.reference}</p>
|
|
94
85
|
</div>
|
|
95
|
-
<span className="text-sm font-bold
|
|
86
|
+
<span className="text-sm font-bold text-aai-primary">
|
|
96
87
|
${booking.price.toLocaleString("en-US")}
|
|
97
88
|
</span>
|
|
98
89
|
</div>
|
|
99
90
|
))}
|
|
100
|
-
<div
|
|
101
|
-
className="flex justify-between border-t pt-2 text-sm font-bold"
|
|
102
|
-
style={{ borderColor: theme.border }}
|
|
103
|
-
>
|
|
91
|
+
<div className="flex justify-between border-t border-aai-border pt-2 text-sm font-bold">
|
|
104
92
|
<span>Total</span>
|
|
105
|
-
<span
|
|
93
|
+
<span className="text-aai-primary">${trip.total.toLocaleString("en-US")}</span>
|
|
106
94
|
</div>
|
|
107
95
|
</div>
|
|
108
96
|
)}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
activeAssistant,
|
|
25
25
|
applyPending,
|
|
26
26
|
describeAction,
|
|
27
|
+
gateFlow,
|
|
27
28
|
note,
|
|
28
29
|
SPECIALISTS,
|
|
29
30
|
type SpecialistId,
|
|
@@ -111,28 +112,46 @@ export const completeOrEscalate = tripSlot.updateTool({
|
|
|
111
112
|
* halts before a sensitive tool and resumes on approval; the halt here is that
|
|
112
113
|
* every sensitive tool stages instead of acting, so the approval has somewhere
|
|
113
114
|
* to arrive.
|
|
115
|
+
*
|
|
116
|
+
* **`when: "awaitingConfirmation"` is the other side of that halt.** It used to
|
|
117
|
+
* be a null check inside {@link applyPending} — "Nothing is waiting for
|
|
118
|
+
* confirmation. Use the booking tool first." — which is the question "where is
|
|
119
|
+
* this conversation", asked of the payload. The gate asks the machine instead,
|
|
120
|
+
* and its refusal carries the state's own instruction. `SETTLED` fires only when
|
|
121
|
+
* the body did NOT answer with a {@link ToolFailure}, so an application that
|
|
122
|
+
* failed leaves the change staged and the caller still being asked.
|
|
114
123
|
*/
|
|
115
|
-
export const confirmAction =
|
|
124
|
+
export const confirmAction = gateFlow.tool({
|
|
116
125
|
description:
|
|
117
126
|
"Apply the change the caller has just confirmed out loud. Only call this " +
|
|
118
127
|
"after you have read the change back and heard a clear yes.",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
when: "awaitingConfirmation",
|
|
129
|
+
send: { type: "SETTLED" },
|
|
130
|
+
execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
|
|
122
131
|
});
|
|
123
132
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `cancel_action` — the caller said no. Drops the staged action, changes nothing.
|
|
135
|
+
*
|
|
136
|
+
* Gated for the same reason as `confirm_action`, and its own "nothing was
|
|
137
|
+
* waiting" arm is gone with the same argument.
|
|
138
|
+
*/
|
|
139
|
+
export const cancelAction = gateFlow.tool({
|
|
126
140
|
description:
|
|
127
141
|
"Discard the change the caller just declined. Call this when they say no, " +
|
|
128
142
|
"or when they want to change the details before confirming.",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
when: "awaitingConfirmation",
|
|
144
|
+
send: { type: "SETTLED" },
|
|
145
|
+
execute: (_args, ctx) =>
|
|
146
|
+
tripSlot.update(ctx, (trip) => {
|
|
147
|
+
const action = trip.pending;
|
|
148
|
+
// Reachable only if the position and the payload disagree; see
|
|
149
|
+
// `applyPending`. Reported rather than thrown, mid-call.
|
|
150
|
+
if (!action) return { discarded: null, message: "Nothing was staged after all." };
|
|
151
|
+
trip.pending = null;
|
|
152
|
+
const described = describeAction(action);
|
|
153
|
+
const summary = typeof described === "string" ? described : action.kind;
|
|
154
|
+
note(trip, `Declined: ${summary}`);
|
|
155
|
+
return { discarded: summary, message: "Nothing was changed." };
|
|
156
|
+
}),
|
|
138
157
|
});
|