@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
|
@@ -11,6 +11,7 @@ function makeState(authenticatedUserId: string | null): RetailState {
|
|
|
11
11
|
callSeq: 3,
|
|
12
12
|
activity: [{ seq: 3, tool: "get_order_details", summary: "read #W5866402", at: 0 }],
|
|
13
13
|
focus: { orderId: "#W5866402" },
|
|
14
|
+
pending: null,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -196,3 +197,44 @@ describe("buildScriptBullets", () => {
|
|
|
196
197
|
expect(bullets.length).toBeLessThanOrEqual(6);
|
|
197
198
|
});
|
|
198
199
|
});
|
|
200
|
+
|
|
201
|
+
describe("the staged change in the projection", () => {
|
|
202
|
+
function stagedState(authenticatedUserId: string | null): RetailState {
|
|
203
|
+
const state = makeState(authenticatedUserId);
|
|
204
|
+
state.pending = {
|
|
205
|
+
kind: "cancel_pending_order",
|
|
206
|
+
plan: {
|
|
207
|
+
readBack: "cancel order #W5866402 and refund $12.00 to gift_card_7794233",
|
|
208
|
+
orderId: "#W5866402",
|
|
209
|
+
reason: "no longer needed",
|
|
210
|
+
refunds: [{ methodId: "gift_card_7794233", amount: 12 }],
|
|
211
|
+
total: 12,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return state;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
test("carries the sentence the agent is supposed to be reading back", () => {
|
|
218
|
+
const view = storeView(stagedState("olivia_ito_3591"));
|
|
219
|
+
expect(view.pending?.kind).toBe("cancel_pending_order");
|
|
220
|
+
expect(view.pending?.readBack).toContain("#W5866402");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("is null when nothing is waiting", () => {
|
|
224
|
+
expect(storeView(makeState("olivia_ito_3591")).pending).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("is withheld before authentication, like everything else here", () => {
|
|
228
|
+
// Unreachable in practice — a change cannot be staged before the caller is
|
|
229
|
+
// identified — but the projection does not lean on that: it gates `pending`
|
|
230
|
+
// on `user` exactly as it gates the orders it describes.
|
|
231
|
+
expect(storeView(stagedState(null)).pending).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("the script offers the answer to the question, not a new request", () => {
|
|
235
|
+
expect(buildScriptBullets(stagedState("olivia_ito_3591"))).toEqual([
|
|
236
|
+
'"Yes, go ahead"',
|
|
237
|
+
`"No, don't do that"`,
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DeepReadonly } from "@alexkroman1/aai";
|
|
2
|
+
import type { PendingAction } from "./pending.ts";
|
|
2
3
|
|
|
3
4
|
// ─── Store types ─────────────────────────────────────────────────────────────
|
|
4
5
|
// Field names are tau2's snake_case verbatim, because `seed.json` is tau2's
|
|
@@ -129,6 +130,15 @@ export interface RetailState {
|
|
|
129
130
|
callSeq: number;
|
|
130
131
|
activity: ActivityEntry[];
|
|
131
132
|
focus: { orderId?: string; productId?: string };
|
|
133
|
+
/**
|
|
134
|
+
* The change waiting on the caller's explicit yes, or null.
|
|
135
|
+
*
|
|
136
|
+
* Lives here and not in the flow because `callFlow` holds a POSITION —
|
|
137
|
+
* "something is waiting" — and this holds the thing itself, which is what
|
|
138
|
+
* `confirm_change` applies and what the sidebar renders. One tool call always
|
|
139
|
+
* moves both. See `pending.ts`.
|
|
140
|
+
*/
|
|
141
|
+
pending: PendingAction | null;
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
/**
|
|
@@ -159,6 +169,7 @@ export function emptyRetailState(): RetailState {
|
|
|
159
169
|
callSeq: 0,
|
|
160
170
|
activity: [],
|
|
161
171
|
focus: {},
|
|
172
|
+
pending: null,
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -204,6 +215,14 @@ export interface SwapOptionView {
|
|
|
204
215
|
alternatives: { itemId: string; options: Record<string, string>; price: number }[];
|
|
205
216
|
}
|
|
206
217
|
|
|
218
|
+
/** The staged change, as the browser sees it: what it is and what the agent is
|
|
219
|
+
* supposed to be reading back. The plan itself stays on the server — the panel
|
|
220
|
+
* only has to show the caller's own words back to them. */
|
|
221
|
+
export interface PendingView {
|
|
222
|
+
kind: PendingAction["kind"];
|
|
223
|
+
readBack: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
207
226
|
export interface StoreView {
|
|
208
227
|
customer: CustomerView | null;
|
|
209
228
|
orders: OrderView[];
|
|
@@ -214,6 +233,10 @@ export interface StoreView {
|
|
|
214
233
|
activity: ActivityEntry[];
|
|
215
234
|
scriptBullets: string[];
|
|
216
235
|
productCount: number;
|
|
236
|
+
/** Gated on `user` like everything else here: a change staged before the
|
|
237
|
+
* caller was identified cannot exist, but the projection does not rely on
|
|
238
|
+
* that — it withholds it the same way it withholds their orders. */
|
|
239
|
+
pending: PendingView | null;
|
|
217
240
|
}
|
|
218
241
|
|
|
219
242
|
export const MAX_SWAP_ITEMS = 3;
|
|
@@ -397,6 +420,10 @@ export function storeView(state: FrozenRetailState): StoreView {
|
|
|
397
420
|
...(state.focus.productId ? { productId: state.focus.productId } : {}),
|
|
398
421
|
},
|
|
399
422
|
swapOptions: swapOptionsFor(state, focusedOrder),
|
|
423
|
+
pending:
|
|
424
|
+
user && state.pending
|
|
425
|
+
? { kind: state.pending.kind, readBack: state.pending.plan.readBack }
|
|
426
|
+
: null,
|
|
400
427
|
callSeq: state.callSeq,
|
|
401
428
|
activity: state.activity.slice(-MAX_ACTIVITY),
|
|
402
429
|
scriptBullets: buildScriptBullets(state),
|
|
@@ -417,6 +444,14 @@ export function buildScriptBullets(state: FrozenRetailState): string[] {
|
|
|
417
444
|
const userId = state.authenticatedUserId;
|
|
418
445
|
const user = userId ? state.store.users[userId] : undefined;
|
|
419
446
|
|
|
447
|
+
// A staged change is a QUESTION the agent has just asked, so while one waits
|
|
448
|
+
// the only two useful things a caller can say are the answer to it. Offering
|
|
449
|
+
// "cancel my pending order" here would be offering an action the gate is
|
|
450
|
+
// about to refuse.
|
|
451
|
+
if (user && state.pending) {
|
|
452
|
+
return ['"Yes, go ahead"', '"No, don\'t do that"'];
|
|
453
|
+
}
|
|
454
|
+
|
|
420
455
|
// Pre-auth: only the two ways in. The personas panel (a constant, rendered
|
|
421
456
|
// straight from DEMO_PERSONAS) covers WHICH customer to be.
|
|
422
457
|
if (!user) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
2
|
-
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
2
|
+
import { createToolContext, ok, okPosition } from "@alexkroman1/aai/testing";
|
|
3
3
|
import { describe, expect, test } from "vitest";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import {
|
|
6
6
|
authenticatedUser,
|
|
7
|
+
BEFORE_TRANSFER,
|
|
8
|
+
callFlow,
|
|
7
9
|
createDefaultState,
|
|
8
10
|
findItem,
|
|
9
11
|
findOrder,
|
|
@@ -163,15 +165,16 @@ describe("retailTool", () => {
|
|
|
163
165
|
name: "echo",
|
|
164
166
|
description: "test tool",
|
|
165
167
|
inputSchema: z.object({ value: z.string() }),
|
|
166
|
-
|
|
168
|
+
when: BEFORE_TRANSFER,
|
|
167
169
|
summary: (args) => `echoed ${args.value}`,
|
|
168
170
|
execute: (args) => ({ echoed: args.value }),
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
const gated = retailTool({
|
|
172
174
|
name: "gated",
|
|
173
|
-
description: "test tool needing
|
|
175
|
+
description: "test tool needing an identified customer",
|
|
174
176
|
inputSchema: z.object({}),
|
|
177
|
+
when: "serving",
|
|
175
178
|
summary: () => "ran",
|
|
176
179
|
execute: () => ({ ok: true }),
|
|
177
180
|
});
|
|
@@ -180,11 +183,15 @@ describe("retailTool", () => {
|
|
|
180
183
|
name: "failing",
|
|
181
184
|
description: "test tool that returns an error",
|
|
182
185
|
inputSchema: z.object({}),
|
|
183
|
-
|
|
186
|
+
when: BEFORE_TRANSFER,
|
|
184
187
|
summary: () => "should not be used",
|
|
185
188
|
execute: () => ({ error: "nope" }),
|
|
186
189
|
});
|
|
187
190
|
|
|
191
|
+
/** Put the call where a `when: "serving"` tool can run, through the flow
|
|
192
|
+
* rather than by writing the store — the gate reads the machine. */
|
|
193
|
+
const serve = (ctx: ToolContext) => callFlow.send(ctx, { type: "IDENTIFIED" });
|
|
194
|
+
|
|
188
195
|
test("increments callSeq and logs activity on every call", async () => {
|
|
189
196
|
const ctx = makeCtx();
|
|
190
197
|
await echo.execute({ value: "a" }, ctx);
|
|
@@ -203,27 +210,45 @@ describe("retailTool", () => {
|
|
|
203
210
|
expect(retailSlot.get(ctx).callSeq).toBeGreaterThan(first);
|
|
204
211
|
});
|
|
205
212
|
|
|
206
|
-
test("
|
|
213
|
+
test("a tool gated on serving refuses while the caller is unidentified", async () => {
|
|
207
214
|
const ctx = makeCtx();
|
|
208
215
|
const result = await gated.execute({}, ctx);
|
|
209
|
-
expect(isToolFailure(result)
|
|
216
|
+
expect(isToolFailure(result)).toBe(true);
|
|
217
|
+
// The refusal is the SDK's, so it names the position and quotes the
|
|
218
|
+
// state's own instruction — where `requiresAuth` answered one fixed
|
|
219
|
+
// sentence that could not say where the call was.
|
|
220
|
+
expect(isToolFailure(result) && result.error).toContain('"identifying"');
|
|
221
|
+
expect(isToolFailure(result) && result.error).toMatch(/user id/);
|
|
210
222
|
});
|
|
211
223
|
|
|
212
|
-
test("a
|
|
224
|
+
test("a refused call touches nothing, callSeq included", async () => {
|
|
213
225
|
const ctx = makeCtx();
|
|
214
226
|
await gated.execute({}, ctx);
|
|
215
227
|
const state = retailSlot.get(ctx);
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
// The gate short-circuits before the wrapper's body, so the EXECUTION path
|
|
229
|
+
// writes nothing at all — which is what makes a refusal unable to half-write
|
|
230
|
+
// the store. The sidebar line for a blocked call comes from `agent.ts`'s
|
|
231
|
+
// `tool.called` hook instead, which is a different path and not under test
|
|
232
|
+
// here: see `registry.test.ts`.
|
|
233
|
+
expect(state.callSeq).toBe(0);
|
|
234
|
+
expect(state.activity).toEqual([]);
|
|
218
235
|
});
|
|
219
236
|
|
|
220
|
-
test("runs once
|
|
237
|
+
test("runs once the flow says serving", async () => {
|
|
221
238
|
const ctx = makeCtx();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
serve(ctx);
|
|
240
|
+
expect(ok(await gated.execute({}, ctx))).toEqual({ ok: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("the result carries the position the call landed in", async () => {
|
|
244
|
+
const ctx = makeCtx();
|
|
245
|
+
serve(ctx);
|
|
246
|
+
// `okPosition` rather than a cast to `{ instruction?: string }`: it keeps
|
|
247
|
+
// the envelope this test is about, and a refusal fails here naming what the
|
|
248
|
+
// flow said instead of reading `undefined` off a field nobody assigned.
|
|
249
|
+
const answered = okPosition<{ ok: boolean }>(await gated.execute({}, ctx));
|
|
250
|
+
expect(answered).toMatchObject({ state: "serving.helping", done: false });
|
|
251
|
+
expect(answered.instruction).toMatch(/one identified customer/);
|
|
227
252
|
});
|
|
228
253
|
|
|
229
254
|
test("an error result is logged as an error, not through summary()", async () => {
|
|
@@ -232,6 +257,21 @@ describe("retailTool", () => {
|
|
|
232
257
|
expect(retailSlot.get(ctx).activity[0]?.summary).toBe("error: nope");
|
|
233
258
|
});
|
|
234
259
|
|
|
260
|
+
test("a body that failed does not move the call", async () => {
|
|
261
|
+
const ctx = makeCtx();
|
|
262
|
+
const moves = retailTool({
|
|
263
|
+
name: "moves",
|
|
264
|
+
description: "test tool that would identify the caller and fails instead",
|
|
265
|
+
inputSchema: z.object({}),
|
|
266
|
+
when: BEFORE_TRANSFER,
|
|
267
|
+
send: { type: "IDENTIFIED" },
|
|
268
|
+
summary: () => "moved",
|
|
269
|
+
execute: () => ({ error: "no such customer" }),
|
|
270
|
+
});
|
|
271
|
+
expect(isToolFailure(await moves.execute({}, ctx))).toBe(true);
|
|
272
|
+
expect(callFlow.position(ctx).state).toBe("identifying");
|
|
273
|
+
});
|
|
274
|
+
|
|
235
275
|
test("activity is capped so a long call cannot grow the payload", async () => {
|
|
236
276
|
const ctx = makeCtx();
|
|
237
277
|
for (let i = 0; i < 15; i++) await echo.execute({ value: String(i) }, ctx);
|
|
@@ -249,3 +289,32 @@ describe("retailTool", () => {
|
|
|
249
289
|
expect(new Set(state.activity.map((a) => a.seq)).size).toBe(state.activity.length);
|
|
250
290
|
});
|
|
251
291
|
});
|
|
292
|
+
|
|
293
|
+
describe("the call flow", () => {
|
|
294
|
+
test("a fresh call is identifying, and nothing is latched", () => {
|
|
295
|
+
const ctx = makeCtx();
|
|
296
|
+
expect(callFlow.position(ctx).state).toBe("identifying");
|
|
297
|
+
expect(retailSlot.get(ctx).authenticatedUserId).toBeNull();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("transferred is final, so every tool refuses after the handoff", async () => {
|
|
301
|
+
const ctx = makeCtx();
|
|
302
|
+
const anywhere = retailTool({
|
|
303
|
+
name: "anywhere",
|
|
304
|
+
description: "legal in every state but the terminal one",
|
|
305
|
+
inputSchema: z.object({}),
|
|
306
|
+
when: BEFORE_TRANSFER,
|
|
307
|
+
summary: () => "ran",
|
|
308
|
+
execute: () => ({ ok: true }),
|
|
309
|
+
});
|
|
310
|
+
expect(ok(await anywhere.execute({}, ctx))).toEqual({ ok: true });
|
|
311
|
+
|
|
312
|
+
const at = callFlow.send(ctx, { type: "TRANSFERRED" });
|
|
313
|
+
expect(at.state).toBe("transferred");
|
|
314
|
+
expect(at.done).toBe(true);
|
|
315
|
+
|
|
316
|
+
const refused = await anywhere.execute({}, ctx);
|
|
317
|
+
expect(isToolFailure(refused)).toBe(true);
|
|
318
|
+
expect(isToolFailure(refused) && refused.error).toContain('"transferred"');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolContext, ToolFailure } from "@alexkroman1/aai";
|
|
2
|
-
import { isToolFailure, pushCapped, sessionSlot
|
|
2
|
+
import { dialog, isToolFailure, omitUndefined, pushCapped, sessionSlot } from "@alexkroman1/aai";
|
|
3
3
|
import type { z } from "zod";
|
|
4
4
|
import seedJson from "./seed.json";
|
|
5
5
|
import type {
|
|
@@ -71,6 +71,108 @@ export function seedStore(): Store {
|
|
|
71
71
|
*/
|
|
72
72
|
export const retailSlot = sessionSlot("retail", createDefaultState);
|
|
73
73
|
|
|
74
|
+
// ─── The call, as a machine ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Where this call is, and what may be done from here, as a plain state map.
|
|
78
|
+
*
|
|
79
|
+
* A {@link DialogSpec} rather than an XState machine: everything this dialog
|
|
80
|
+
* says is states, events and an instruction each, which is exactly what a spec
|
|
81
|
+
* can say. The instruction is a declared field instead of an untyped `meta`
|
|
82
|
+
* bag, so misspelling it is a compile error rather than a refusal that arrives
|
|
83
|
+
* with no recovery text. `as const` is what keeps the `on` keys literal, so
|
|
84
|
+
* `send` below is checked against the events this spec actually declares.
|
|
85
|
+
*
|
|
86
|
+
* Three of the policy's sections live here rather than in prose.
|
|
87
|
+
*
|
|
88
|
+
* **"Authenticate first."** `identifying`'s instruction names both finder
|
|
89
|
+
* tools, and ten tools declare `when: "serving"`, so the gate is the SDK's.
|
|
90
|
+
*
|
|
91
|
+
* **"Handing off to a human."** The policy says to call
|
|
92
|
+
* `transfer_to_human_agents` and then say exactly one sentence "and nothing
|
|
93
|
+
* else" — which nothing checked, so every tool stayed callable after the handoff
|
|
94
|
+
* and a model that kept going kept acting on a call it had already given away.
|
|
95
|
+
* `transferred` is a `final` state: no tool declares itself legal there, so
|
|
96
|
+
* every one of them refuses. `TRANSFERRED` sits on the `serving` PARENT so it
|
|
97
|
+
* is reachable from either child — a caller asking for a human mid-confirmation
|
|
98
|
+
* must get one.
|
|
99
|
+
*
|
|
100
|
+
* **"Confirm every change out loud."** This was the last rule in the policy
|
|
101
|
+
* carried by nothing at all, and it is `serving`'s two children now. Nothing in
|
|
102
|
+
* this template mutates the store except `confirm_change`, which is legal only
|
|
103
|
+
* in `awaitingConfirmation`, which is reachable only by staging a change. See
|
|
104
|
+
* `pending.ts` for what that buys and what it still cannot promise.
|
|
105
|
+
*
|
|
106
|
+
* **`IDENTIFIED` is deliberately NOT declared on `serving`.** It was, while
|
|
107
|
+
* `serving` was a leaf, so that a caller repeating their email did not hit an
|
|
108
|
+
* error. Now that `serving` has children, a self-transition would RE-ENTER it
|
|
109
|
+
* and reset to `helping` — stranding a staged change that `state.pending` still
|
|
110
|
+
* holds, which is the one way the position and the store could come to
|
|
111
|
+
* disagree. An event no active state handles is ignored, and ignoring it is
|
|
112
|
+
* exactly right: re-identifying the same customer does not move the call.
|
|
113
|
+
* `authenticateAs` is what refuses a switch to a DIFFERENT customer. Whether
|
|
114
|
+
* the call is identified and WHO it is identified as are two facts: this holds
|
|
115
|
+
* the first, `authenticatedUserId` holds the second.
|
|
116
|
+
*/
|
|
117
|
+
const callSpec = {
|
|
118
|
+
initial: "identifying",
|
|
119
|
+
states: {
|
|
120
|
+
identifying: {
|
|
121
|
+
// The instruction NAMES the two tools, because this sentence is what a
|
|
122
|
+
// refusal quotes and a refusal is the model's recovery path.
|
|
123
|
+
instruction:
|
|
124
|
+
"You do not know who this is yet. Identify the caller with " +
|
|
125
|
+
"find_user_id_by_email, or find_user_id_by_name_zip if they cannot " +
|
|
126
|
+
"remember the email. Do this even if they volunteer a user id.",
|
|
127
|
+
on: { IDENTIFIED: "serving", TRANSFERRED: "transferred" },
|
|
128
|
+
},
|
|
129
|
+
serving: {
|
|
130
|
+
initial: "helping",
|
|
131
|
+
on: { TRANSFERRED: "transferred" },
|
|
132
|
+
states: {
|
|
133
|
+
helping: {
|
|
134
|
+
instruction:
|
|
135
|
+
"You are helping one identified customer, and only that one. Nothing is " +
|
|
136
|
+
"waiting on their word. A tool that changes something only STAGES it and " +
|
|
137
|
+
"hands you a sentence to read back.",
|
|
138
|
+
on: { STAGED: "awaitingConfirmation" },
|
|
139
|
+
},
|
|
140
|
+
awaitingConfirmation: {
|
|
141
|
+
instruction:
|
|
142
|
+
"A change is staged and NOTHING HAS HAPPENED YET. Read the staged sentence " +
|
|
143
|
+
"back — the order, the items, the amounts, where the money goes — and wait " +
|
|
144
|
+
"for an explicit yes. Then call confirm_change, or cancel_change if they " +
|
|
145
|
+
"say no or want to change any part of it.",
|
|
146
|
+
on: { SETTLED: "helping" },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
transferred: {
|
|
151
|
+
final: true,
|
|
152
|
+
instruction:
|
|
153
|
+
"The call belongs to a human agent now. Say nothing beyond the transfer " +
|
|
154
|
+
"sentence, and do nothing else.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
} as const;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The flow. Its own slot key beside {@link retailSlot}: the flow holds the
|
|
161
|
+
* POSITION and the store holds the customer, the orders, the activity feed and
|
|
162
|
+
* the staged change itself, because an inspectable {@link PendingAction} is
|
|
163
|
+
* what `confirm_change` applies. One tool call always moves both — a staging
|
|
164
|
+
* tool writes `pending` and sends `STAGED` in the same synchronous window.
|
|
165
|
+
*/
|
|
166
|
+
export const callFlow = dialog("call", callSpec);
|
|
167
|
+
|
|
168
|
+
/** Every state a tool may run in before the call is handed to a human — i.e.
|
|
169
|
+
* everything but `transferred`. What the two finders, the three catalog reads
|
|
170
|
+
* and the transfer itself declare, so the terminal state gates them without an
|
|
171
|
+
* auth gate. `"serving"` matches both of its children, which is what keeps a
|
|
172
|
+
* read legal while a change is waiting: a caller who asks "what was the total
|
|
173
|
+
* again?" mid-confirmation must be answerable. */
|
|
174
|
+
export const BEFORE_TRANSFER = ["identifying", "serving"] as const;
|
|
175
|
+
|
|
74
176
|
export function setFocus(
|
|
75
177
|
state: RetailState,
|
|
76
178
|
focus: { orderId?: string; productId?: string },
|
|
@@ -128,12 +230,26 @@ export function findPaymentMethod(user: User, methodId: string): PaymentMethod |
|
|
|
128
230
|
|
|
129
231
|
// ─── Guards ──────────────────────────────────────────────────────────────────
|
|
130
232
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
233
|
+
/**
|
|
234
|
+
* The customer on this call.
|
|
235
|
+
*
|
|
236
|
+
* The null arm is reachable only if the POSITION and the STORE disagree — the
|
|
237
|
+
* flow says `serving` while nothing latched a user id — which no code path
|
|
238
|
+
* produces, since `authenticateAs` is what both writes the id and lets the
|
|
239
|
+
* `IDENTIFIED` event through. It is kept and reported rather than thrown for the
|
|
240
|
+
* reason `travel-concierge`'s `cancel_action` keeps its own: this runs mid-call,
|
|
241
|
+
* and a sentence the model can act on beats an exception. The GATE that a
|
|
242
|
+
* customer is identified at all is `callFlow`'s, declared per tool as `when`.
|
|
243
|
+
*/
|
|
135
244
|
export function authenticatedUser(state: RetailState): User | ToolFailure {
|
|
136
|
-
if (!state.authenticatedUserId)
|
|
245
|
+
if (!state.authenticatedUserId) {
|
|
246
|
+
return {
|
|
247
|
+
error:
|
|
248
|
+
"No customer is latched onto this call yet. Identify them with " +
|
|
249
|
+
"find_user_id_by_email, or find_user_id_by_name_zip if they cannot " +
|
|
250
|
+
"remember their email.",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
137
253
|
return findUser(state, state.authenticatedUserId);
|
|
138
254
|
}
|
|
139
255
|
|
|
@@ -187,24 +303,52 @@ interface RetailToolSpec<S extends z.ZodType<Record<string, unknown>>, R> {
|
|
|
187
303
|
/** Required even for no-arg tools — pass `z.object({})`. One code path in the
|
|
188
304
|
* wrapper is worth more than saving a line at one call site. */
|
|
189
305
|
inputSchema: S;
|
|
190
|
-
/** Default true. Only the two finder tools and the three catalog tools opt
|
|
191
|
-
* out; everything else touches customer data. */
|
|
192
|
-
requiresAuth?: boolean;
|
|
193
|
-
summary: (args: z.output<S>, result: R) => string;
|
|
194
306
|
/**
|
|
195
|
-
*
|
|
307
|
+
* The state(s) this tool may run in, as `callFlow`'s states spell them.
|
|
308
|
+
*
|
|
309
|
+
* Replaces the `requiresAuth` boolean this spec used to carry. Nine tools
|
|
310
|
+
* want `"serving"` — which matches both of its children, so a read and a
|
|
311
|
+
* staging tool are equally legal while a change waits; the two settling
|
|
312
|
+
* tools want `"serving.awaitingConfirmation"`; and the two finders, the three
|
|
313
|
+
* catalog reads and the transfer want {@link BEFORE_TRANSFER}, which is every
|
|
314
|
+
* state but the terminal one — so "does not need a customer" and "is still
|
|
315
|
+
* legal after the handoff" stopped being the same claim, and they were never
|
|
316
|
+
* the same claim.
|
|
317
|
+
*/
|
|
318
|
+
when: string | readonly string[];
|
|
319
|
+
/** The event to send once the body has succeeded, for a tool that MOVES the
|
|
320
|
+
* call. Twelve do: the two finders send `IDENTIFIED`, the transfer sends
|
|
321
|
+
* `TRANSFERRED`, the seven staging tools send `STAGED`, and `confirm_change`
|
|
322
|
+
* and `cancel_change` send `SETTLED`. Nothing is sent when the body answers
|
|
323
|
+
* a `ToolFailure` — which is what stops a refused stage from moving the call
|
|
324
|
+
* into a confirmation with nothing behind it. */
|
|
325
|
+
send?:
|
|
326
|
+
| { type: "IDENTIFIED" }
|
|
327
|
+
| { type: "TRANSFERRED" }
|
|
328
|
+
| { type: "STAGED" }
|
|
329
|
+
| { type: "SETTLED" };
|
|
330
|
+
/**
|
|
331
|
+
* One line for the activity feed, from the call that SUCCEEDED.
|
|
196
332
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* `result
|
|
201
|
-
* `
|
|
202
|
-
* fifteen tool files then stops compiling, or worse, stops meaning anything.
|
|
203
|
-
* It lives here rather than in each tool file because it is a property of
|
|
204
|
-
* this type: the same four lines were pasted into eight of the fifteen and
|
|
205
|
-
* pointed at from five more, which is a rule maintained in fourteen places.
|
|
333
|
+
* `R` is the SUCCESS type — `execute` below is declared `R | ToolFailure`, so
|
|
334
|
+
* the failure arm is matched against `ToolFailure` during inference and never
|
|
335
|
+
* lands in `R`. That is what lets the fifteen tool files stop writing
|
|
336
|
+
* `isToolFailure(result) ? "… failed" : …` for a case that cannot arrive: the
|
|
337
|
+
* wrapper never calls `summary` on a failure and now says so in the type.
|
|
206
338
|
*
|
|
207
|
-
*
|
|
339
|
+
* `NoInfer` takes this parameter out of the inference race `R` used to be
|
|
340
|
+
* decided by. It does NOT make the source order irrelevant — `execute` is an
|
|
341
|
+
* inline arrow whose parameters are contextually typed, so its return type is
|
|
342
|
+
* inferred in a LATER pass than this signature is checked, and a `summary`
|
|
343
|
+
* written above it still lands on `unknown`. What changed is that the failure
|
|
344
|
+
* is now LOUD: with no `isToolFailure` ternary left to swallow it, the wrong
|
|
345
|
+
* order is a `TS18046` on the first property read rather than a narrowing
|
|
346
|
+
* that quietly stops meaning anything. So the twelve-line warning this used
|
|
347
|
+
* to carry is the compiler's job now.
|
|
348
|
+
*/
|
|
349
|
+
summary: (args: z.output<S>, result: NoInfer<R>) => string;
|
|
350
|
+
/**
|
|
351
|
+
* Handed the store as its second argument, and SYNCHRONOUS.
|
|
208
352
|
*
|
|
209
353
|
* **The draft is passed in rather than re-read**, which is the one change the
|
|
210
354
|
* durable store forced on this template. The body used to open with
|
|
@@ -218,10 +362,16 @@ interface RetailToolSpec<S extends z.ZodType<Record<string, unknown>>, R> {
|
|
|
218
362
|
* so none of them wants to; a tool that DID would await outside the wrapper and
|
|
219
363
|
* call `retailSlot.update` itself, the way `plan-and-execute`'s do.
|
|
220
364
|
*/
|
|
221
|
-
execute: (args: z.output<S>, state: RetailState, ctx: ToolContext) => R;
|
|
365
|
+
execute: (args: z.output<S>, state: RetailState, ctx: ToolContext) => R | ToolFailure;
|
|
222
366
|
}
|
|
223
367
|
|
|
224
|
-
|
|
368
|
+
/**
|
|
369
|
+
* One line of the activity feed, and the `callSeq` bump the sidebar moves on.
|
|
370
|
+
*
|
|
371
|
+
* Exported because {@link retailTool} is no longer its only caller: `agent.ts`
|
|
372
|
+
* records the calls that never reach a body — see {@link gateFor}.
|
|
373
|
+
*/
|
|
374
|
+
export function record(state: RetailState, name: string, summary: string): void {
|
|
225
375
|
state.callSeq += 1;
|
|
226
376
|
pushCapped(
|
|
227
377
|
state.activity,
|
|
@@ -231,13 +381,46 @@ function record(state: RetailState, name: string, summary: string): void {
|
|
|
231
381
|
}
|
|
232
382
|
|
|
233
383
|
/**
|
|
234
|
-
*
|
|
235
|
-
* may re-implement:
|
|
384
|
+
* Which states each retail tool may run in, by the name the model calls it by.
|
|
236
385
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
386
|
+
* Populated as the fifteen `tools/` modules are loaded, because {@link retailTool}
|
|
387
|
+
* is what every one of them calls. It exists so `agent.ts`'s `tool.called` hook
|
|
388
|
+
* can ask the SAME question the gate asks — this is one fact evaluated twice, not
|
|
389
|
+
* two copies of a fact.
|
|
390
|
+
*/
|
|
391
|
+
const TOOL_GATES = new Map<string, readonly string[]>();
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The states `name` may run in, or `undefined` for a tool this template did not
|
|
395
|
+
* declare (a builtin, or anything a future author adds outside the wrapper).
|
|
396
|
+
*/
|
|
397
|
+
export function gateFor(name: string): readonly string[] | undefined {
|
|
398
|
+
return TOOL_GATES.get(name);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Every retail tool is built through this. It owns two things no tool body may
|
|
403
|
+
* re-implement:
|
|
404
|
+
*
|
|
405
|
+
* 1. the mutation window the body's draft comes from,
|
|
406
|
+
* 2. the `callSeq` increment + activity entry — the reason the UI moves on
|
|
407
|
+
* every tool call rather than only when a projected value happens to differ.
|
|
408
|
+
*
|
|
409
|
+
* The third thing it used to own — the authentication gate — is
|
|
410
|
+
* {@link callFlow}'s now, declared per tool as `when`. What that buys is in the
|
|
411
|
+
* machine's own doc; what it COST was one line of the activity feed, because a
|
|
412
|
+
* refused call short-circuits before this wrapper's body runs and so recorded
|
|
413
|
+
* nothing and bumped no `callSeq`.
|
|
414
|
+
*
|
|
415
|
+
* **That line is back, and not from here.** `agent.ts` declares a `tool.called`
|
|
416
|
+
* hook, which the runtime emits for every call the model makes INCLUDING the
|
|
417
|
+
* ones the gate goes on to refuse — a place to observe from that a tool wrapper
|
|
418
|
+
* structurally does not have. See {@link gateFor}.
|
|
419
|
+
*
|
|
420
|
+
* **`callFlow.tool` rather than `retailSlot.updateTool`**, so the body opens the
|
|
421
|
+
* store's window itself. A flow tool's own `execute` is handed `(args, ctx)`;
|
|
422
|
+
* everything else about a tool body here is unchanged, including that it is
|
|
423
|
+
* synchronous — the window cannot span an await.
|
|
241
424
|
*
|
|
242
425
|
* `focus` is deliberately left to tool bodies (`setFocus`): it is a UI nicety,
|
|
243
426
|
* not an invariant, and only the body knows what the call was about.
|
|
@@ -245,26 +428,26 @@ function record(state: RetailState, name: string, summary: string): void {
|
|
|
245
428
|
export function retailTool<S extends z.ZodType<Record<string, unknown>>, R>(
|
|
246
429
|
spec: RetailToolSpec<S, R>,
|
|
247
430
|
) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// the body inside the slot's mutation window and hands it the draft, so this
|
|
251
|
-
// wrapper is left with only what is specific to THIS agent.
|
|
252
|
-
return retailSlot.updateTool({
|
|
431
|
+
TOOL_GATES.set(spec.name, typeof spec.when === "string" ? [spec.when] : spec.when);
|
|
432
|
+
return callFlow.tool({
|
|
253
433
|
description: spec.description,
|
|
254
434
|
inputSchema: spec.inputSchema,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
spec.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
435
|
+
when: spec.when,
|
|
436
|
+
// `omitUndefined` rather than a conditional spread: `exactOptionalPropertyTypes`
|
|
437
|
+
// is on, so an explicit `send: undefined` is not the same as an absent one —
|
|
438
|
+
// and only three of the fifteen tools declare an event (guard-invariants
|
|
439
|
+
// rule 2).
|
|
440
|
+
...omitUndefined({ send: spec.send }),
|
|
441
|
+
execute: (args, ctx) =>
|
|
442
|
+
retailSlot.update(ctx, (state) => {
|
|
443
|
+
const typedArgs = args as z.output<S>;
|
|
444
|
+
const result = spec.execute(typedArgs, state, ctx);
|
|
445
|
+
record(
|
|
446
|
+
state,
|
|
447
|
+
spec.name,
|
|
448
|
+
isToolFailure(result) ? `error: ${result.error}` : spec.summary(typedArgs, result),
|
|
449
|
+
);
|
|
450
|
+
return result;
|
|
451
|
+
}),
|
|
269
452
|
});
|
|
270
453
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { createDefaultState, findUser } from "./store.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { applySwapLines, assertCanCoverDiff, planItemSwap, toSwapLines } from "./swap.ts";
|
|
5
5
|
|
|
6
6
|
function fixture(orderId: string) {
|
|
7
7
|
const state = createDefaultState();
|
|
@@ -152,7 +152,7 @@ describe("assertCanCoverDiff", () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
describe("
|
|
155
|
+
describe("applySwapLines", () => {
|
|
156
156
|
test("each swapped line takes its OWN new price and options", () => {
|
|
157
157
|
const { state, order } = fixture("#W9311069");
|
|
158
158
|
// Two different products in one call — this is what catches the leaked
|
|
@@ -165,7 +165,7 @@ describe("applySwap", () => {
|
|
|
165
165
|
{ requireDifferent: true },
|
|
166
166
|
);
|
|
167
167
|
if (isToolFailure(plan)) throw new Error(plan.error);
|
|
168
|
-
|
|
168
|
+
applySwapLines(order, toSwapLines(plan));
|
|
169
169
|
|
|
170
170
|
const vacuum = order.items.find((i) => i.item_id === "4725166838");
|
|
171
171
|
const kettle = order.items.find((i) => i.item_id === "3909406921");
|
|
@@ -183,7 +183,7 @@ describe("applySwap", () => {
|
|
|
183
183
|
requireDifferent: true,
|
|
184
184
|
});
|
|
185
185
|
if (isToolFailure(plan)) throw new Error(plan.error);
|
|
186
|
-
|
|
186
|
+
applySwapLines(order, toSwapLines(plan));
|
|
187
187
|
expect(order.items.map((i) => i.item_id)).toEqual(["3909406921", "7292993796"]);
|
|
188
188
|
});
|
|
189
189
|
});
|