@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.
Files changed (122) hide show
  1. package/README.md +1 -0
  2. package/dist/{_bundler-B4RqNF5Z.mjs → _bundler-CDuYl5Gb.mjs} +1 -1
  3. package/dist/{_dev-server-CiKFH9qw.mjs → _dev-server-Dt3TCjhj.mjs} +6 -2
  4. package/dist/{_init-CE4HKdgV.mjs → _init-BhEfQ6Yi.mjs} +1 -1
  5. package/dist/_resource-commands.d.ts +23 -0
  6. package/dist/{_slug-api-nRFaBEPJ.mjs → _slug-api-z-USxPtL.mjs} +14 -10
  7. package/dist/_slug-api.d.ts +5 -2
  8. package/dist/{_templates-FI3xPEMj.mjs → _templates-4WcKOjS5.mjs} +4 -5
  9. package/dist/_templates.d.ts +3 -4
  10. package/dist/{build-6jfaakgc.mjs → build-BBxf8vom.mjs} +2 -2
  11. package/dist/cli.mjs +338 -346
  12. package/dist/{deploy-DvYeD8ia.mjs → deploy-DRC8jWr8.mjs} +1 -1
  13. package/dist/{dev-B59JIfWO.mjs → dev-QuuOOUtv.mjs} +1 -1
  14. package/dist/eval-DfQ8cm5p.mjs +56 -0
  15. package/dist/eval.d.ts +43 -0
  16. package/dist/{init-CpJVjEg0.mjs → init-CUo87gcI.mjs} +2 -2
  17. package/dist/{logs-9q-psa4q.mjs → logs-CQ2T46dr.mjs} +1 -1
  18. package/dist/scaffold/CLAUDE.md +409 -68
  19. package/dist/scaffold/package.json +5 -4
  20. package/dist/scaffold/server.mjs +10 -3
  21. package/dist/scaffold/tsconfig.json +0 -1
  22. package/dist/{secret-CVvSLIDV.mjs → secret-Ct34kdgU.mjs} +1 -1
  23. package/dist/{storage-BvUrnvM3.mjs → storage-i_79ZwAR.mjs} +1 -1
  24. package/dist/{studio--MUV0cid.mjs → studio-CzTzPqe1.mjs} +1 -1
  25. package/dist/templates/briefing-desk/agent.test.ts +350 -0
  26. package/dist/templates/briefing-desk/agent.ts +30 -0
  27. package/dist/templates/briefing-desk/shared.ts +200 -0
  28. package/dist/templates/briefing-desk/system-prompt.md +39 -0
  29. package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
  30. package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
  31. package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
  32. package/dist/templates/call-audit/agent.eval.test.ts +474 -0
  33. package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
  34. package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
  35. package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
  36. package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
  37. package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
  38. package/dist/templates/infocom-adventure/agent.test.ts +49 -18
  39. package/dist/templates/infocom-adventure/agent.ts +21 -0
  40. package/dist/templates/infocom-adventure/shared.ts +20 -9
  41. package/dist/templates/infocom-adventure/system-prompt.md +1 -1
  42. package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
  43. package/dist/templates/link-digest/agent.eval.test.ts +195 -0
  44. package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
  45. package/dist/templates/night-owl/agent.eval.test.ts +272 -0
  46. package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
  47. package/dist/templates/personal-finance/system-prompt.md +4 -0
  48. package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
  49. package/dist/templates/pipeline-simple/agent.ts +2 -2
  50. package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
  51. package/dist/templates/pizza-ordering/agent.test.ts +15 -0
  52. package/dist/templates/pizza-ordering/shared.ts +20 -1
  53. package/dist/templates/pizza-ordering/system-prompt.md +6 -0
  54. package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
  55. package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
  56. package/dist/templates/podcast-digest/agent.test.ts +30 -59
  57. package/dist/templates/podcast-digest/agent.ts +1 -1
  58. package/dist/templates/podcast-digest/workflows/digest.ts +52 -19
  59. package/dist/templates/podcast-digest/workflows/slack.ts +48 -169
  60. package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
  61. package/dist/templates/redline/agent.eval.test.ts +209 -0
  62. package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
  63. package/dist/templates/retail/address.ts +87 -1
  64. package/dist/templates/retail/agent.eval.test.ts +314 -0
  65. package/dist/templates/retail/agent.test.ts +171 -91
  66. package/dist/templates/retail/agent.ts +35 -1
  67. package/dist/templates/retail/cancel.ts +116 -0
  68. package/dist/templates/retail/client.tsx +39 -0
  69. package/dist/templates/retail/payment.ts +107 -0
  70. package/dist/templates/retail/pending.test.ts +196 -0
  71. package/dist/templates/retail/pending.ts +137 -0
  72. package/dist/templates/retail/registry.test.ts +119 -8
  73. package/dist/templates/retail/returns.ts +102 -0
  74. package/dist/templates/retail/shared.test.ts +42 -0
  75. package/dist/templates/retail/shared.ts +35 -0
  76. package/dist/templates/retail/store.test.ts +6 -4
  77. package/dist/templates/retail/store.ts +113 -57
  78. package/dist/templates/retail/swap.test.ts +4 -4
  79. package/dist/templates/retail/swap.ts +248 -15
  80. package/dist/templates/retail/system-prompt.md +62 -30
  81. package/dist/templates/retail/tools/cancel_change.ts +33 -0
  82. package/dist/templates/retail/tools/cancel_pending_order.ts +15 -59
  83. package/dist/templates/retail/tools/confirm_change.ts +42 -0
  84. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +21 -66
  85. package/dist/templates/retail/tools/modify_pending_order_address.ts +12 -24
  86. package/dist/templates/retail/tools/modify_pending_order_items.ts +22 -60
  87. package/dist/templates/retail/tools/modify_pending_order_payment.ts +15 -77
  88. package/dist/templates/retail/tools/modify_user_address.ts +12 -15
  89. package/dist/templates/retail/tools/return_delivered_order_items.ts +16 -71
  90. package/dist/templates/retail/tools/transfer_to_human_agents.ts +4 -2
  91. package/dist/templates/simple/agent.eval.test.ts +54 -0
  92. package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
  93. package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
  94. package/dist/templates/support-line/agent.eval.test.ts +273 -0
  95. package/dist/templates/support-line/system-prompt.md +2 -1
  96. package/dist/templates/support-line/tools/answer_question.ts +56 -7
  97. package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
  98. package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
  99. package/dist/templates/travel-concierge/agent.test.ts +110 -10
  100. package/dist/templates/travel-concierge/agent.ts +18 -9
  101. package/dist/templates/travel-concierge/shared.ts +110 -9
  102. package/dist/templates/travel-concierge/system-prompt.md +7 -1
  103. package/dist/templates/travel-concierge/tools/book_car_rental.ts +6 -3
  104. package/dist/templates/travel-concierge/tools/book_excursion.ts +6 -3
  105. package/dist/templates/travel-concierge/tools/book_hotel.ts +6 -3
  106. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +6 -3
  107. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
  108. package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
  109. package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
  110. package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
  111. package/dist/templates/travel-concierge/tools/update_ticket.ts +6 -3
  112. package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
  113. package/dist/templates/web-researcher/system-prompt.md +15 -4
  114. package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
  115. package/dist/test.d.ts +29 -5
  116. package/dist/{worker-bundler-COxnqstQ.mjs → worker-bundler-CCVEDjm1.mjs} +45 -25
  117. package/dist/worker-bundler.mjs +1 -1
  118. package/dist/{workflow-D2AQf2Pl.mjs → workflow-BflATMmU.mjs} +19 -11
  119. package/package.json +5 -5
  120. package/dist/eject-C9WJyyr2.mjs +0 -116
  121. package/dist/eject.d.ts +0 -60
  122. package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
@@ -10,16 +10,33 @@ next. Read it — it is the shortest true statement of what you may do, and you
10
10
  not have to remember it between turns. A tool that is not available yet refuses
11
11
  outright and tells you what has to happen first.
12
12
 
13
- # Authenticate first
13
+ Three of the rules below are that machine rather than prose, and they are the
14
+ three you cannot get wrong by forgetting: you cannot act before you know who is
15
+ calling, you cannot act after the call has gone to a human, and you cannot
16
+ change anything the customer has not said yes to.
14
17
 
15
- Before anything else, identify who you are talking to by finding their user id
16
- — by email (`find_user_id_by_email`), or by first name, last name and zip code
17
- (`find_user_id_by_name_zip`) if they cannot remember the email. Do this **even
18
- if the caller volunteers their user id**. Until you have, every other tool will
19
- refuse.
18
+ # Nothing changes until the customer says yes
20
19
 
21
- Prefer email. Fall back to name + zip only when they cannot recall the address
22
- on the account.
20
+ **No tool here changes anything.** Cancelling, modifying, returning and
21
+ exchanging all do the same thing: they check the request, price it, and hand
22
+ you one sentence describing exactly what would happen. Nothing has happened at
23
+ that point.
24
+
25
+ So every change is three steps, and they are three separate turns:
26
+
27
+ 1. Call the tool for the change. It answers with a sentence.
28
+ 2. **Say that sentence to the customer** — the order, the items, the amounts,
29
+ where the money is going — and stop. Ask them plainly: is that right?
30
+ 3. On an explicit "yes", call `confirm_change`. On anything else — "no",
31
+ hesitation, "wait", a correction, a new subject — call `cancel_change` and
32
+ start again from what they actually want.
33
+
34
+ Never treat silence, a "mm-hm" in the middle of your sentence, or the fact that
35
+ they asked for it a minute ago as a yes. If you are not sure they agreed, ask
36
+ again; nothing is lost by asking, because nothing has happened yet.
37
+
38
+ Only one change can be waiting at a time. If you need to stage a different one,
39
+ settle the first with `confirm_change` or `cancel_change`.
23
40
 
24
41
  # One customer per call
25
42
 
@@ -28,12 +45,9 @@ requests from that person, but you must refuse anything to do with anybody
28
45
  else's account, and you cannot switch to a different customer mid-call — ask
29
46
  them to call back.
30
47
 
31
- # Confirm every change out loud
32
-
33
- Before any action that changes something cancel, modify, return, exchange
34
- say what you are about to do, including the order, the items, the amounts and
35
- where money is going, and wait for an explicit "yes". Never act on an implied
36
- yes.
48
+ Identify them by email (`find_user_id_by_email`), or by first name, last name
49
+ and zip code (`find_user_id_by_name_zip`) if they cannot remember the email.
50
+ Prefer email. Do this **even if the caller volunteers their user id**.
37
51
 
38
52
  # Never invent anything
39
53
 
@@ -51,10 +65,23 @@ speak; when you speak, do not also make a tool call.
51
65
  # Handing off to a human
52
66
 
53
67
  Transfer only if the caller explicitly asks for a human, or their request
54
- cannot be handled with your tools and this policy. Call
55
- `transfer_to_human_agents` first, then say exactly: "You are being transferred
56
- to a human agent. Please hold on." Say nothing else after that. The call is over
57
- for you at that point, and every tool will refuse including that one.
68
+ cannot be handled with your tools and this policy.
69
+
70
+ **The handoff is a tool call, not a sentence.** Every turn in which the customer
71
+ asks for a person "a real person", "an agent", "a supervisor", "put me
72
+ through", "this isn't working" — begins with a call to
73
+ `transfer_to_human_agents`. Not a question back, not an offer to try once more,
74
+ and not the sentence below. Do not ask what the problem is first, and do not try
75
+ to talk them out of it.
76
+
77
+ **Never say "You are being transferred" unless `transfer_to_human_agents` has
78
+ already answered in this turn.** Saying it without the call leaves the customer
79
+ holding a line nobody is coming to: nothing has been handed anywhere, and the
80
+ call is still yours. That is the one mistake here you cannot take back.
81
+
82
+ Once the tool has answered, say exactly: "You are being transferred to a human
83
+ agent. Please hold on." Say nothing else after that. The call is over for you at
84
+ that point, and every tool will refuse — including that one.
58
85
 
59
86
  # Speaking on the phone
60
87
 
@@ -67,6 +94,9 @@ for you at that point, and every tool will refuse — including that one.
67
94
  - When a customer gives you a number, read it back before you use it.
68
95
  - Prices: "three hundred and twenty dollars and fifty cents", not "320.50".
69
96
  - Keep replies to one or two sentences. This is a phone call, not an email.
97
+ The one place to be longer is the readback in step 2 above — that sentence
98
+ earns its length, and rushing it is the whole failure mode it exists to
99
+ prevent.
70
100
  - Do not spell out tool names, statuses in snake_case, or user ids.
71
101
 
72
102
  # What the tools accept
@@ -80,22 +110,25 @@ product's options.
80
110
 
81
111
  # The rules that bite
82
112
 
113
+ Every rule here is checked when you stage a change, so a request that breaks one
114
+ is refused before the customer is ever asked to agree to it. They are written
115
+ out so you can steer the conversation, not because you have to enforce them.
116
+
83
117
  **All times in the store's records are EST, 24-hour.** "02:30:00" is 2:30 in
84
118
  the morning.
85
119
 
86
120
  **Cancelling a pending order.** Only an order whose status is exactly
87
- "pending". Check the status first. The reason must be either "no longer needed"
88
- or "ordered by mistake" — if the customer gives another reason, tell them those
89
- are the only two we can record and ask which fits. A gift-card refund lands
90
- immediately; everything else takes 5 to 7 business days.
121
+ "pending". The reason must be either "no longer needed" or "ordered by mistake"
122
+ — if the customer gives another reason, tell them those are the only two we can
123
+ record and ask which fits. A gift-card refund lands immediately; everything else
124
+ takes 5 to 7 business days.
91
125
 
92
126
  **Changing the items in a pending order is once-only and irreversible.** After
93
127
  it, the order cannot be cancelled or modified again — not by you, not by
94
- anyone. So before you call it: get the customer to confirm they have named
95
- **every** item they want changed, then read the complete list and the price
96
- difference back. Ask "is that everything you want to change?" explicitly. An
97
- item can only become a different option of the same product — a shirt cannot
98
- become shoes.
128
+ anyone. So before you stage it: get the customer to confirm they have named
129
+ **every** item they want changed. Ask "is that everything you want to change?"
130
+ explicitly, and only then stage the whole list in one call. An item can only
131
+ become a different option of the same product — a shirt cannot become shoes.
99
132
 
100
133
  **Changing a pending order's payment method.** One method only, and it must be
101
134
  different from the current one. A gift card must cover the whole order total.
@@ -103,11 +136,10 @@ The old method is refunded.
103
136
 
104
137
  **Returning a delivered order.** Only "delivered", only once. The refund goes
105
138
  to the original payment method or to one of their gift cards — nothing else.
106
- Confirm the exact item list. The customer gets an email about sending things
107
- back.
139
+ The customer gets an email about sending things back.
108
140
 
109
141
  **Exchanging a delivered order.** Only "delivered", only once. Same-product
110
- options only. Remind them to name every item first, exactly as with modifying a
142
+ options only. Ask them to name every item first, exactly as with modifying a
111
143
  pending order. The price difference is charged to or refunded from a payment
112
144
  method they choose; a gift card must cover a positive difference. No new order
113
145
  is needed.
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { retailTool } from "../store.ts";
3
+
4
+ /**
5
+ * Drop the staged change and go back to helping.
6
+ *
7
+ * Clears `pending` UNCONDITIONALLY and always sends `SETTLED`, so this is the
8
+ * way out of `awaitingConfirmation` from any state the store could be in — a
9
+ * caller who says no, changes their mind about one item, or wants to talk about
10
+ * something else entirely must never leave the call wedged on a change nobody
11
+ * is going to make.
12
+ */
13
+ export default retailTool({
14
+ name: "cancel_change",
15
+ when: "serving.awaitingConfirmation",
16
+ send: { type: "SETTLED" },
17
+ description:
18
+ "Drop the change that is waiting, without applying it. Use this when the caller says no, " +
19
+ "hesitates, wants any part of it different, or moves on to something else. Nothing was " +
20
+ "changed, so there is nothing to undo — stage the corrected change afterwards if they want one.",
21
+ inputSchema: z.object({}),
22
+ execute: (_args, state) => {
23
+ const dropped = state.pending;
24
+ state.pending = null;
25
+ return {
26
+ dropped: dropped?.kind ?? null,
27
+ message: dropped
28
+ ? `Dropped — nothing was changed. The caller did not agree to: ${dropped.plan.readBack}.`
29
+ : "Nothing was staged, so nothing was dropped.",
30
+ };
31
+ },
32
+ summary: (_args, result) => `dropped ${result.dropped ?? "nothing"}`,
33
+ });
@@ -1,20 +1,20 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { creditRefund, REFUND_DELAY_NOTE, REFUND_IMMEDIATE_NOTE } from "../refund.ts";
4
- import { OrderIdField, resolveOrder } from "../resolve.ts";
5
- import { authenticatedUser, retailTool, setFocus } from "../store.ts";
6
-
7
- /** tau2 accepts exactly these two. Anything else is refused. */
8
- const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
3
+ import { CANCEL_REASONS, planCancel } from "../cancel.ts";
4
+ import { stageAction } from "../pending.ts";
5
+ import { OrderIdField } from "../resolve.ts";
6
+ import { retailTool, setFocus } from "../store.ts";
9
7
 
10
8
  export default retailTool({
11
9
  name: "cancel_pending_order",
12
10
  when: "serving",
11
+ send: { type: "STAGED" },
13
12
  description:
14
- "Cancel a pending order. Only an order whose status is exactly 'pending' can be cancelled " +
15
- "check the status first. The reason must be either 'no longer needed' or 'ordered by mistake'. " +
16
- "State the order, its total and the refund destination to the caller and get an explicit yes " +
17
- "before calling this.",
13
+ "STAGE a cancellation of a pending order this does NOT cancel anything. Only an order whose " +
14
+ "status is exactly 'pending' can be cancelled, and the reason must be either 'no longer " +
15
+ "needed' or 'ordered by mistake'. The order, its total and where the refund goes come back as " +
16
+ "a sentence to read to the caller; nothing happens until you hear an explicit yes and call " +
17
+ "confirm_change.",
18
18
  inputSchema: z.object({
19
19
  order_id: OrderIdField,
20
20
  reason: z
@@ -22,54 +22,10 @@ export default retailTool({
22
22
  .describe("Either 'no longer needed' or 'ordered by mistake' — no other reason is accepted"),
23
23
  }),
24
24
  execute: (args, state) => {
25
- const user = authenticatedUser(state);
26
- if (isToolFailure(user)) return user;
27
-
28
- const order = resolveOrder(state, args.order_id);
29
- if (isToolFailure(order)) return order;
30
- setFocus(state, { orderId: order.order_id });
31
-
32
- if (order.status !== "pending") {
33
- return {
34
- error: `Order ${order.order_id} is ${order.status}, and only a pending order can be cancelled.`,
35
- };
36
- }
37
- // The enum makes this unreachable from a well-formed call; it stays because
38
- // the reason is the one field a caller supplies in free speech.
39
- if (!CANCEL_REASONS.includes(args.reason)) {
40
- return {
41
- error: `'${args.reason}' is not an accepted cancellation reason. It must be 'no longer needed' or 'ordered by mistake'.`,
42
- };
43
- }
44
-
45
- let immediate = false;
46
- const refunds = order.payment_history
47
- .filter((payment) => payment.transaction_type === "payment")
48
- .map((payment) => {
49
- const credited = creditRefund(user, payment.payment_method_id, payment.amount);
50
- immediate = immediate || credited.immediate;
51
- return {
52
- transaction_type: "refund" as const,
53
- amount: payment.amount,
54
- payment_method_id: payment.payment_method_id,
55
- };
56
- });
57
-
58
- order.status = "cancelled";
59
- order.cancel_reason = args.reason;
60
- order.payment_history.push(...refunds);
61
-
62
- const total = refunds.reduce((sum, refund) => sum + refund.amount, 0);
63
- return {
64
- order_id: order.order_id,
65
- status: order.status,
66
- cancel_reason: order.cancel_reason,
67
- refunded: total,
68
- refund_immediate: immediate,
69
- message: `Order ${order.order_id} is cancelled and $${total.toFixed(2)} is being refunded. ${
70
- immediate ? REFUND_IMMEDIATE_NOTE : REFUND_DELAY_NOTE
71
- }`,
72
- };
25
+ const plan = planCancel(state, args.order_id, args.reason);
26
+ if (isToolFailure(plan)) return plan;
27
+ setFocus(state, { orderId: plan.orderId });
28
+ return stageAction(state, { kind: "cancel_pending_order", plan });
73
29
  },
74
- summary: (_args, result) => `cancelled ${result.order_id}`,
30
+ summary: (_args, result) => `staged: ${result.read_back}`,
75
31
  });
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { applyAction } from "../pending.ts";
3
+ import { retailTool } from "../store.ts";
4
+
5
+ /**
6
+ * The only tool in this template that writes to the store.
7
+ *
8
+ * Gated on `serving.awaitingConfirmation`, which is reachable only by staging a
9
+ * change — so "confirm what nobody staged" is refused by the SDK before this
10
+ * body runs, and the refusal quotes the state's own instruction. See
11
+ * `pending.ts`.
12
+ */
13
+ export default retailTool({
14
+ name: "confirm_change",
15
+ when: "serving.awaitingConfirmation",
16
+ send: { type: "SETTLED" },
17
+ description:
18
+ "Apply the change that is waiting, after you have read it back to the caller and heard an " +
19
+ "explicit yes. Never call this on an implied yes, on silence, or before you have said what " +
20
+ "the change is. If they say no, or want any part of it different, call cancel_change instead.",
21
+ inputSchema: z.object({}),
22
+ execute: (_args, state) => {
23
+ const action = state.pending;
24
+ // Unreachable while the gate holds: `awaitingConfirmation` is entered only
25
+ // by a staging tool, which writes `pending` in the same synchronous window
26
+ // it sends `STAGED` in. Kept and reported rather than thrown for the reason
27
+ // `authenticatedUser` keeps its own — this runs mid-call, and a sentence
28
+ // the model can act on beats an exception. `cancel_change` is the way out
29
+ // if it ever did fire, which is why that tool clears unconditionally.
30
+ if (!action) {
31
+ return {
32
+ error:
33
+ "Nothing is staged, so there is nothing to confirm. Stage the change again with the " +
34
+ "tool for it, read it back, and confirm once the caller says yes.",
35
+ };
36
+ }
37
+ const result = applyAction(state, action);
38
+ state.pending = null;
39
+ return { confirmed: action.kind, ...result };
40
+ },
41
+ summary: (_args, result) => `confirmed ${result.confirmed}`,
42
+ });
@@ -1,18 +1,21 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { OrderIdField, resolveOrder } from "../resolve.ts";
4
- import { authenticatedUser, money, retailTool, setFocus } from "../store.ts";
5
- import { assertCanCoverDiff, planItemSwap } from "../swap.ts";
3
+ import { stageAction } from "../pending.ts";
4
+ import { OrderIdField } from "../resolve.ts";
5
+ import { retailTool, setFocus } from "../store.ts";
6
+ import { planExchange } from "../swap.ts";
6
7
 
7
8
  export default retailTool({
8
9
  name: "exchange_delivered_order_items",
9
10
  when: "serving",
11
+ send: { type: "STAGED" },
10
12
  description:
11
- "Request an exchange of items in a delivered order for different options of the SAME products. " +
12
- "Only a 'delivered' order can be exchanged, and only once. Remind the caller to name EVERY " +
13
- "item they want exchanged before you call this, read the whole list and the price difference " +
14
- "back, and get an explicit yes. No new order is needed the customer gets an email explaining " +
15
- "how to send the originals back.",
13
+ "STAGE an exchange of items in a delivered order for different options of the SAME products " +
14
+ "this does NOT request anything. Only a 'delivered' order can be exchanged, and only once, so " +
15
+ "remind the caller to name EVERY item they want exchanged before you call this. The whole " +
16
+ "list and the price difference come back as a sentence to read to them; nothing happens until " +
17
+ "you hear an explicit yes and call confirm_change. No new order is needed — the customer gets " +
18
+ "an email explaining how to send the originals back.",
16
19
  inputSchema: z.object({
17
20
  order_id: OrderIdField,
18
21
  item_ids: z
@@ -29,64 +32,16 @@ export default retailTool({
29
32
  .describe("Method to charge or refund the price difference"),
30
33
  }),
31
34
  execute: (args, state) => {
32
- const user = authenticatedUser(state);
33
- if (isToolFailure(user)) return user;
34
-
35
- const order = resolveOrder(state, args.order_id);
36
- if (isToolFailure(order)) return order;
37
- setFocus(state, { orderId: order.order_id });
38
-
39
- if (order.status !== "delivered") {
40
- return {
41
- error: `Order ${order.order_id} is ${order.status}. Only a delivered order can be exchanged, and only once.`,
42
- };
43
- }
44
-
45
- // requireDifferent is false: tau2 permits a same-item entry here, and a
46
- // zero-difference exchange is harmless on a delivered order.
47
- const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
48
- requireDifferent: false,
49
- });
35
+ const plan = planExchange(
36
+ state,
37
+ args.order_id,
38
+ args.item_ids,
39
+ args.new_item_ids,
40
+ args.payment_method_id,
41
+ );
50
42
  if (isToolFailure(plan)) return plan;
51
-
52
- const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
53
- if (blocked) return blocked;
54
-
55
- // No balance moves and no items change: this records a REQUEST. The money
56
- // and the item swap settle when the originals come back.
57
- order.status = "exchange requested";
58
- // SORTED INDEPENDENTLY, verbatim from tau2 (`order.exchange_items =
59
- // sorted(item_ids)` / `sorted(new_item_ids)`, the same shape
60
- // `return_delivered_order_items` uses one list of) — kept because
61
- // `seed.json` is tau2's data and these fields are what a tau2 task's
62
- // expected end state is compared against. They are therefore two SETS of
63
- // ids, not a pairing: sorting each list separately permutes the second
64
- // against the first whenever the caller named more than one item.
65
- //
66
- // Which is why the ANSWER carries `exchanges` instead. `planItemSwap`
67
- // priced this positionally, so returning the two sorted lists as if they
68
- // lined up told the caller a pairing the quote was not for — the one thing
69
- // this result must not do, since the model reads it back down a phone.
70
- order.exchange_items = [...args.item_ids].sort();
71
- order.exchange_new_items = [...args.new_item_ids].sort();
72
- order.exchange_payment_method_id = args.payment_method_id;
73
- order.exchange_price_difference = plan.diff;
74
-
75
- return {
76
- order_id: order.order_id,
77
- status: order.status,
78
- price_difference: plan.diff,
79
- // The pairing AS PRICED, per line, in the order the caller gave it.
80
- exchanges: plan.pairs.map((pair) => ({
81
- item_id: pair.item.item_id,
82
- new_item_id: pair.newVariant.item_id,
83
- price_difference: money(pair.newVariant.price - pair.item.price),
84
- })),
85
- message:
86
- plan.diff > 0
87
- ? `Exchange requested on ${order.order_id}. $${plan.diff.toFixed(2)} will be charged to ${args.payment_method_id}. An email with return instructions is on its way.`
88
- : `Exchange requested on ${order.order_id}. $${Math.abs(plan.diff).toFixed(2)} will be refunded to ${args.payment_method_id}. An email with return instructions is on its way.`,
89
- };
43
+ setFocus(state, { orderId: plan.orderId });
44
+ return stageAction(state, { kind: "exchange_delivered_order_items", plan });
90
45
  },
91
- summary: (_args, result) => `exchange requested on ${result.order_id}`,
46
+ summary: (_args, result) => `staged: ${result.read_back}`,
92
47
  });
@@ -1,39 +1,27 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { AddressFields, formatAddress, toAddress } from "../address.ts";
4
- import { OrderIdField, resolveOrder } from "../resolve.ts";
3
+ import { AddressFields, planOrderAddress } from "../address.ts";
4
+ import { stageAction } from "../pending.ts";
5
+ import { OrderIdField } from "../resolve.ts";
5
6
  import { retailTool, setFocus } from "../store.ts";
6
7
 
7
8
  export default retailTool({
8
9
  name: "modify_pending_order_address",
9
10
  when: "serving",
11
+ send: { type: "STAGED" },
10
12
  description:
11
- "Change the shipping address of a pending order. Read the new address back to the caller and " +
12
- "get an explicit yes before calling this. This does not change the customer's default address.",
13
+ "STAGE a change to a pending order's shipping address this does NOT change it. The new " +
14
+ "address comes back as a sentence to read to the caller; nothing happens until you hear an " +
15
+ "explicit yes and call confirm_change. This does not touch the customer's default address.",
13
16
  inputSchema: z.object({
14
17
  order_id: OrderIdField,
15
18
  ...AddressFields,
16
19
  }),
17
20
  execute: (args, state) => {
18
- const order = resolveOrder(state, args.order_id);
19
- if (isToolFailure(order)) return order;
20
- setFocus(state, { orderId: order.order_id });
21
-
22
- // Any pending variant is fine here — unlike cancel and modify-items, which
23
- // require exactly 'pending'. Re-addressing a modified order is harmless.
24
- if (!order.status.startsWith("pending")) {
25
- return {
26
- error: `Order ${order.order_id} is ${order.status}, and only a pending order's address can be changed.`,
27
- };
28
- }
29
-
30
- order.address = toAddress(args);
31
- return {
32
- order_id: order.order_id,
33
- status: order.status,
34
- address: order.address,
35
- message: `Order ${order.order_id} now ships to ${formatAddress(order.address)}.`,
36
- };
21
+ const plan = planOrderAddress(state, args.order_id, args);
22
+ if (isToolFailure(plan)) return plan;
23
+ setFocus(state, { orderId: plan.orderId });
24
+ return stageAction(state, { kind: "modify_pending_order_address", plan });
37
25
  },
38
- summary: (_args, result) => `re-addressed ${result.order_id}`,
26
+ summary: (_args, result) => `staged: ${result.read_back}`,
39
27
  });
@@ -1,18 +1,22 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { OrderIdField, resolveOrder } from "../resolve.ts";
4
- import { authenticatedUser, isGiftCard, money, retailTool, setFocus } from "../store.ts";
5
- import { applySwap, assertCanCoverDiff, planItemSwap } from "../swap.ts";
3
+ import { stageAction } from "../pending.ts";
4
+ import { OrderIdField } from "../resolve.ts";
5
+ import { retailTool, setFocus } from "../store.ts";
6
+ import { planModifyItems } from "../swap.ts";
6
7
 
7
8
  export default retailTool({
8
9
  name: "modify_pending_order_items",
9
10
  when: "serving",
11
+ send: { type: "STAGED" },
10
12
  description:
11
- "Change items in a pending order to different options of the SAME products. This can be done " +
12
- "ONCE per order and is irreversible afterwards the order can no longer be cancelled or " +
13
- "modified at all. Collect EVERY item the caller wants changed into one call, read the whole " +
14
- "list and the price difference back, and get an explicit yes first. The item and replacement " +
15
- "lists are positional and must be the same length.",
13
+ "STAGE a change of items in a pending order to different options of the SAME products this " +
14
+ "does NOT change anything. It can only ever be done ONCE per order and is irreversible: " +
15
+ "afterwards the order can no longer be cancelled or modified at all. So collect EVERY item " +
16
+ "the caller wants changed into one call ask them 'is that everything you want to change?' " +
17
+ "explicitly first. The whole list and the price difference come back as a sentence to read " +
18
+ "to them; nothing happens until you hear an explicit yes and call confirm_change. The item " +
19
+ "and replacement lists are positional and must be the same length.",
16
20
  inputSchema: z.object({
17
21
  order_id: OrderIdField,
18
22
  item_ids: z
@@ -29,58 +33,16 @@ export default retailTool({
29
33
  .describe("Method to charge or refund the price difference, e.g. 'gift_card_0000000'"),
30
34
  }),
31
35
  execute: (args, state) => {
32
- const user = authenticatedUser(state);
33
- if (isToolFailure(user)) return user;
34
-
35
- const order = resolveOrder(state, args.order_id);
36
- if (isToolFailure(order)) return order;
37
- setFocus(state, { orderId: order.order_id });
38
-
39
- // Exactly 'pending'. A 'pending (item modified)' order has already used its
40
- // one modification, which is what makes this action terminal.
41
- if (order.status !== "pending") {
42
- return {
43
- error: `Order ${order.order_id} is ${order.status}. Items can only be changed while an order is exactly 'pending', and only once.`,
44
- };
45
- }
46
-
47
- const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
48
- requireDifferent: true,
49
- });
36
+ const plan = planModifyItems(
37
+ state,
38
+ args.order_id,
39
+ args.item_ids,
40
+ args.new_item_ids,
41
+ args.payment_method_id,
42
+ );
50
43
  if (isToolFailure(plan)) return plan;
51
-
52
- const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
53
- if (blocked) return blocked;
54
-
55
- const method = user.payment_methods[args.payment_method_id];
56
- if (method && isGiftCard(method)) {
57
- method.balance = money(method.balance - plan.diff);
58
- }
59
- order.payment_history.push({
60
- transaction_type: plan.diff > 0 ? "payment" : "refund",
61
- amount: money(Math.abs(plan.diff)),
62
- payment_method_id: args.payment_method_id,
63
- });
64
-
65
- applySwap(order, plan);
66
- order.status = "pending (item modified)";
67
-
68
- return {
69
- order_id: order.order_id,
70
- status: order.status,
71
- price_difference: plan.diff,
72
- items: order.items.map((item) => ({
73
- name: item.name,
74
- item_id: item.item_id,
75
- options: item.options,
76
- price: item.price,
77
- })),
78
- message:
79
- plan.diff > 0
80
- ? `Done. $${plan.diff.toFixed(2)} was charged to ${args.payment_method_id}. This order can no longer be modified or cancelled.`
81
- : `Done. $${Math.abs(plan.diff).toFixed(2)} is being refunded to ${args.payment_method_id}. This order can no longer be modified or cancelled.`,
82
- };
44
+ setFocus(state, { orderId: plan.orderId });
45
+ return stageAction(state, { kind: "modify_pending_order_items", plan });
83
46
  },
84
- summary: (_args, result) =>
85
- `modified ${result.order_id} (${result.price_difference >= 0 ? "+" : ""}${result.price_difference})`,
47
+ summary: (_args, result) => `staged: ${result.read_back}`,
86
48
  });