@alexkroman1/aai-cli 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{_bundler-B4RqNF5Z.mjs → _bundler-CDuYl5Gb.mjs} +1 -1
- package/dist/{_dev-server-CiKFH9qw.mjs → _dev-server-Dt3TCjhj.mjs} +6 -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-QuuOOUtv.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
|
@@ -38,8 +38,8 @@ const registry = Object.entries(retailAgent.tools);
|
|
|
38
38
|
// distinct session, which is what these per-tool cases assume.
|
|
39
39
|
const makeCtx = (): ToolContext => createToolContext();
|
|
40
40
|
|
|
41
|
-
/** A context whose call flow is in `serving`, so a `when: "serving"`
|
|
42
|
-
* reach its body. Moved through the FLOW rather than by writing
|
|
41
|
+
/** A context whose call flow is in `serving.helping`, so a `when: "serving"`
|
|
42
|
+
* tool can reach its body. Moved through the FLOW rather than by writing
|
|
43
43
|
* `authenticatedUserId`, because the gate reads the machine. */
|
|
44
44
|
function servingCtx(): ToolContext {
|
|
45
45
|
const ctx = makeCtx();
|
|
@@ -47,11 +47,44 @@ function servingCtx(): ToolContext {
|
|
|
47
47
|
return ctx;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** The two tools legal only while a change waits on the caller's yes. */
|
|
51
|
+
const SETTLING_TOOLS = new Set(["cancel_change", "confirm_change"]);
|
|
52
|
+
|
|
53
|
+
function toolNamed(name: string) {
|
|
54
|
+
const def = retailAgent.tools[name];
|
|
55
|
+
if (!def) throw new Error(`no tool named ${name}`);
|
|
56
|
+
return def;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A context in whatever state `name` needs to reach its BODY.
|
|
61
|
+
*
|
|
62
|
+
* The two settling tools are reached by really identifying a caller and really
|
|
63
|
+
* staging a change, rather than by sending `STAGED` at the machine: they read
|
|
64
|
+
* `state.pending`, and a position with nothing staged behind it is a state this
|
|
65
|
+
* template cannot actually be in. Everything else only needs `serving`.
|
|
66
|
+
*/
|
|
67
|
+
async function bodyReachableCtx(name: string): Promise<ToolContext> {
|
|
68
|
+
if (!SETTLING_TOOLS.has(name)) return servingCtx();
|
|
69
|
+
const ctx = makeCtx();
|
|
70
|
+
await toolNamed("find_user_id_by_email").execute(
|
|
71
|
+
{ email: "aarav.anderson9752@example.com" },
|
|
72
|
+
ctx,
|
|
73
|
+
);
|
|
74
|
+
await toolNamed("cancel_pending_order").execute(
|
|
75
|
+
{ order_id: "#W9300146", reason: "no longer needed" },
|
|
76
|
+
ctx,
|
|
77
|
+
);
|
|
78
|
+
return ctx;
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
/** Minimal args satisfying each tool's schema. Deliberately plausible-shaped
|
|
51
82
|
* but wrong — these calls are expected to fail; what is asserted is that they
|
|
52
83
|
* still moved the UI. */
|
|
53
84
|
const SAMPLE_ARGS: Record<string, Record<string, unknown>> = {
|
|
85
|
+
cancel_change: {},
|
|
54
86
|
cancel_pending_order: { order_id: "#W0000000", reason: "no longer needed" },
|
|
87
|
+
confirm_change: {},
|
|
55
88
|
exchange_delivered_order_items: {
|
|
56
89
|
order_id: "#W0000000",
|
|
57
90
|
item_ids: ["0000000000"],
|
|
@@ -102,10 +135,17 @@ const SAMPLE_ARGS: Record<string, Record<string, unknown>> = {
|
|
|
102
135
|
};
|
|
103
136
|
|
|
104
137
|
describe("tool registry", () => {
|
|
105
|
-
test("registers all
|
|
138
|
+
test("registers all seventeen tools", () => {
|
|
139
|
+
// Fifteen of these are tau2's retail tool set, which this template used to
|
|
140
|
+
// hold verbatim. `confirm_change` and `cancel_change` are the two it does
|
|
141
|
+
// not have: tau2's tools apply on their first call, and here nothing does —
|
|
142
|
+
// see `pending.ts`. Departing from that set is what buys the confirmation
|
|
143
|
+
// gate, and it is the reason this list is no longer a fidelity claim.
|
|
106
144
|
expect(registry.map(([name]) => name).sort()).toEqual(
|
|
107
145
|
[
|
|
146
|
+
"cancel_change",
|
|
108
147
|
"cancel_pending_order",
|
|
148
|
+
"confirm_change",
|
|
109
149
|
"exchange_delivered_order_items",
|
|
110
150
|
"find_user_id_by_email",
|
|
111
151
|
"find_user_id_by_name_zip",
|
|
@@ -144,10 +184,11 @@ describe("the UI-update invariant", () => {
|
|
|
144
184
|
// This is the one that fails if a future tool is built with tool() instead of
|
|
145
185
|
// retailTool(): it would work, and the sidebar would sit still through it.
|
|
146
186
|
test.each(registry)("%s increments callSeq and logs activity", async (name, def) => {
|
|
147
|
-
//
|
|
148
|
-
// point is that a tool which reaches its BODY moves
|
|
149
|
-
// call
|
|
150
|
-
|
|
187
|
+
// In whichever state lets the body run, so the flow gate is not what these
|
|
188
|
+
// calls are testing: the point is that a tool which reaches its BODY moves
|
|
189
|
+
// the sidebar. A refused call never reaches one — the hook below is what
|
|
190
|
+
// moves the sidebar for those.
|
|
191
|
+
const ctx = await bodyReachableCtx(name);
|
|
151
192
|
const before = retailSlot.get(ctx).callSeq;
|
|
152
193
|
await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
|
|
153
194
|
const state = retailSlot.get(ctx);
|
|
@@ -159,12 +200,82 @@ describe("the UI-update invariant", () => {
|
|
|
159
200
|
test.each(registry)("%s logs its own registry key as its name", async (name, def) => {
|
|
160
201
|
// Catches a copy-paste where the retailTool `name` and the registry key
|
|
161
202
|
// disagree — the activity feed would then attribute calls to the wrong tool.
|
|
162
|
-
const ctx =
|
|
203
|
+
const ctx = await bodyReachableCtx(name);
|
|
163
204
|
await def.execute(SAMPLE_ARGS[name] ?? {}, ctx);
|
|
164
205
|
expect(retailSlot.get(ctx).activity.at(-1)?.tool).toBe(name);
|
|
165
206
|
});
|
|
166
207
|
});
|
|
167
208
|
|
|
209
|
+
describe("the blocked-call hook", () => {
|
|
210
|
+
/**
|
|
211
|
+
* A `tool.called`, delivered the way the RUNTIME delivers it.
|
|
212
|
+
*
|
|
213
|
+
* The hook is a plain function on the def, so this needs no harness — and
|
|
214
|
+
* asserting on it here is the only way the blocked lines are covered at all:
|
|
215
|
+
* they are written by something no tool call executes.
|
|
216
|
+
*/
|
|
217
|
+
const called = (name: string, ctx: ToolContext) =>
|
|
218
|
+
retailAgent.events?.["tool.called"]?.(
|
|
219
|
+
{
|
|
220
|
+
type: "tool.called",
|
|
221
|
+
toolCallId: "call_1",
|
|
222
|
+
toolName: name,
|
|
223
|
+
args: {},
|
|
224
|
+
meta: { id: "evt_1", at: 0 },
|
|
225
|
+
},
|
|
226
|
+
ctx,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
test.each(registry.filter(([name]) => !PUBLIC_TOOLS.has(name)))(
|
|
230
|
+
"%s records a blocked line when the model tries it too early",
|
|
231
|
+
(name) => {
|
|
232
|
+
const ctx = makeCtx();
|
|
233
|
+
called(name, ctx);
|
|
234
|
+
|
|
235
|
+
// The regression this closes: the gate moved out of `retailTool` and the
|
|
236
|
+
// sidebar stopped showing the most interesting calls the model makes.
|
|
237
|
+
const state = retailSlot.get(ctx);
|
|
238
|
+
expect(state.callSeq, `${name} recorded no blocked line`).toBe(1);
|
|
239
|
+
expect(state.activity.at(-1)?.tool).toBe(name);
|
|
240
|
+
expect(state.activity.at(-1)?.summary).toContain("blocked");
|
|
241
|
+
expect(state.activity.at(-1)?.summary).toContain("identifying");
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
test.each(registry.filter(([name]) => PUBLIC_TOOLS.has(name)))(
|
|
246
|
+
"%s is left to the wrapper, because it is going to run",
|
|
247
|
+
(name) => {
|
|
248
|
+
const ctx = makeCtx();
|
|
249
|
+
called(name, ctx);
|
|
250
|
+
// The double-count this avoids: a tool that reaches its body records its
|
|
251
|
+
// own line from inside it, with a real summary.
|
|
252
|
+
expect(retailSlot.get(ctx).activity, `${name} was double-recorded`).toEqual([]);
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
test("a tool this template did not declare is ignored", () => {
|
|
257
|
+
const ctx = makeCtx();
|
|
258
|
+
called("web_search", ctx);
|
|
259
|
+
// `gateFor` answers `undefined` for anything not built through `retailTool`
|
|
260
|
+
// — a builtin, or a tool a future author adds outside the wrapper. Recording
|
|
261
|
+
// those would put lines in the feed for calls this gate has no opinion on.
|
|
262
|
+
expect(retailSlot.get(ctx).activity).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("the same tool stops being blocked once the caller is identified", async () => {
|
|
266
|
+
const ctx = makeCtx();
|
|
267
|
+
called("get_user_details", ctx);
|
|
268
|
+
expect(retailSlot.get(ctx).activity).toHaveLength(1);
|
|
269
|
+
|
|
270
|
+
// The hook asks the flow, so it follows the flow: same tool, same session,
|
|
271
|
+
// no line once the position allows it.
|
|
272
|
+
const authed = await bodyReachableCtx("get_user_details");
|
|
273
|
+
const before = retailSlot.get(authed).activity.length;
|
|
274
|
+
called("get_user_details", authed);
|
|
275
|
+
expect(retailSlot.get(authed).activity).toHaveLength(before);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
168
279
|
describe("the authentication gate", () => {
|
|
169
280
|
test.each(registry.filter(([name]) => !PUBLIC_TOOLS.has(name)))(
|
|
170
281
|
"%s refuses before the caller is identified",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returning items from a delivered order, as a plan and an apply (see
|
|
3
|
+
* `cancel.ts` for why every mutating action is split that way).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
|
|
7
|
+
import { resolveOrder } from "./resolve.ts";
|
|
8
|
+
import type { RetailState } from "./shared.ts";
|
|
9
|
+
import { authenticatedUser, findPaymentMethod, isGiftCard } from "./store.ts";
|
|
10
|
+
|
|
11
|
+
export interface ReturnPlan {
|
|
12
|
+
readBack: string;
|
|
13
|
+
orderId: string;
|
|
14
|
+
/** In the order the caller named them — duplicates are meaningful. */
|
|
15
|
+
itemIds: string[];
|
|
16
|
+
/** Item names, positionally aligned with `itemIds`, so the readback and the
|
|
17
|
+
* result can name things rather than read ten-digit numbers aloud. */
|
|
18
|
+
itemNames: string[];
|
|
19
|
+
paymentMethodId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function planReturn(
|
|
23
|
+
state: RetailState,
|
|
24
|
+
spokenOrderId: string,
|
|
25
|
+
itemIds: string[],
|
|
26
|
+
paymentMethodId: string,
|
|
27
|
+
): ReturnPlan | ToolFailure {
|
|
28
|
+
const user = authenticatedUser(state);
|
|
29
|
+
if (isToolFailure(user)) return user;
|
|
30
|
+
|
|
31
|
+
const order = resolveOrder(state, spokenOrderId);
|
|
32
|
+
if (isToolFailure(order)) return order;
|
|
33
|
+
|
|
34
|
+
if (order.status !== "delivered") {
|
|
35
|
+
return {
|
|
36
|
+
error: `Order ${order.order_id} is ${order.status}. Only a delivered order can be returned, and only once.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const method = findPaymentMethod(user, paymentMethodId);
|
|
41
|
+
if (isToolFailure(method)) return method;
|
|
42
|
+
|
|
43
|
+
const originalMethodId = order.payment_history[0]?.payment_method_id;
|
|
44
|
+
if (!isGiftCard(method) && paymentMethodId !== originalMethodId) {
|
|
45
|
+
return {
|
|
46
|
+
error: `A refund must go to the original payment method (${originalMethodId}) or to a gift card. ${paymentMethodId} is neither.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (itemIds.length === 0) {
|
|
51
|
+
return { error: "No items were listed to return." };
|
|
52
|
+
}
|
|
53
|
+
const held = new Map<string, number>();
|
|
54
|
+
for (const item of order.items) {
|
|
55
|
+
held.set(item.item_id, (held.get(item.item_id) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
const asked = new Map<string, number>();
|
|
58
|
+
for (const itemId of itemIds) {
|
|
59
|
+
asked.set(itemId, (asked.get(itemId) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
for (const [itemId, count] of asked) {
|
|
62
|
+
const available = held.get(itemId) ?? 0;
|
|
63
|
+
if (count > available) {
|
|
64
|
+
return {
|
|
65
|
+
error: `Order ${order.order_id} holds ${available} of item ${itemId}, but ${count} were listed for return.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const itemNames = itemIds.map(
|
|
71
|
+
(id) => order.items.find((item) => item.item_id === id)?.name ?? id,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
readBack:
|
|
75
|
+
`return ${itemNames.join(", ")} from order ${order.order_id}, ` +
|
|
76
|
+
`with the refund going to ${paymentMethodId}`,
|
|
77
|
+
orderId: order.order_id,
|
|
78
|
+
itemIds: [...itemIds],
|
|
79
|
+
itemNames,
|
|
80
|
+
paymentMethodId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function applyReturn(state: RetailState, plan: ReturnPlan) {
|
|
85
|
+
const order = state.store.orders[plan.orderId];
|
|
86
|
+
|
|
87
|
+
// In the order the caller named them, not sorted: tau2 sorted this list to
|
|
88
|
+
// match an expected end state, and nothing compares against one any more.
|
|
89
|
+
if (order) {
|
|
90
|
+
order.status = "return requested";
|
|
91
|
+
order.return_items = [...plan.itemIds];
|
|
92
|
+
order.return_payment_method_id = plan.paymentMethodId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
order_id: plan.orderId,
|
|
97
|
+
status: "return requested" as const,
|
|
98
|
+
return_items: plan.itemIds,
|
|
99
|
+
refund_to: plan.paymentMethodId,
|
|
100
|
+
message: `Return requested on ${plan.orderId} for ${plan.itemNames.join(", ")}. The customer will get an email with return instructions, and the refund goes to ${plan.paymentMethodId} once the items arrive.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -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) {
|
|
@@ -225,9 +225,11 @@ describe("retailTool", () => {
|
|
|
225
225
|
const ctx = makeCtx();
|
|
226
226
|
await gated.execute({}, ctx);
|
|
227
227
|
const state = retailSlot.get(ctx);
|
|
228
|
-
// The gate short-circuits before the wrapper's body, so
|
|
229
|
-
//
|
|
230
|
-
//
|
|
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`.
|
|
231
233
|
expect(state.callSeq).toBe(0);
|
|
232
234
|
expect(state.activity).toEqual([]);
|
|
233
235
|
});
|
|
@@ -245,7 +247,7 @@ describe("retailTool", () => {
|
|
|
245
247
|
// the envelope this test is about, and a refusal fails here naming what the
|
|
246
248
|
// flow said instead of reading `undefined` off a field nobody assigned.
|
|
247
249
|
const answered = okPosition<{ ok: boolean }>(await gated.execute({}, ctx));
|
|
248
|
-
expect(answered).toMatchObject({ state: "serving", done: false });
|
|
250
|
+
expect(answered).toMatchObject({ state: "serving.helping", done: false });
|
|
249
251
|
expect(answered.instruction).toMatch(/one identified customer/);
|
|
250
252
|
});
|
|
251
253
|
|