@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.
Files changed (234) hide show
  1. package/README.md +1 -0
  2. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  3. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
  4. package/dist/_dev-env.d.ts +22 -2
  5. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
  6. package/dist/_dev-server.d.ts +0 -17
  7. package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
  8. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  9. package/dist/_preflight.d.ts +1 -1
  10. package/dist/_resource-commands.d.ts +23 -0
  11. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
  12. package/dist/_slug-api.d.ts +5 -2
  13. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  14. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
  15. package/dist/_templates.d.ts +3 -4
  16. package/dist/_workflow-scan.d.ts +108 -0
  17. package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
  18. package/dist/cli.mjs +346 -349
  19. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  20. package/dist/client-bundler.mjs +1 -1
  21. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  22. package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
  23. package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
  24. package/dist/eval-DfQ8cm5p.mjs +56 -0
  25. package/dist/eval.d.ts +43 -0
  26. package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
  27. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  28. package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
  29. package/dist/project-config.mjs +1 -1
  30. package/dist/scaffold/CLAUDE.md +442 -80
  31. package/dist/scaffold/package.json +6 -3
  32. package/dist/scaffold/server.mjs +11 -4
  33. package/dist/scaffold/tsconfig.json +0 -1
  34. package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
  35. package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
  36. package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
  37. package/dist/templates/briefing-desk/agent.test.ts +350 -0
  38. package/dist/templates/briefing-desk/agent.ts +30 -0
  39. package/dist/templates/briefing-desk/shared.ts +200 -0
  40. package/dist/templates/briefing-desk/system-prompt.md +39 -0
  41. package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
  42. package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
  43. package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
  44. package/dist/templates/call-audit/agent.eval.test.ts +474 -0
  45. package/dist/templates/call-audit/agent.test.ts +57 -204
  46. package/dist/templates/call-audit/agent.ts +32 -19
  47. package/dist/templates/call-audit/client.tsx +17 -54
  48. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  49. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  50. package/dist/templates/call-audit/workflows/media.ts +2 -12
  51. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  52. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  53. package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
  54. package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
  55. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  56. package/dist/templates/dispatch-center/client.tsx +239 -129
  57. package/dist/templates/dispatch-center/shared.ts +99 -1
  58. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  59. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  60. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  61. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  62. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  63. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  66. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  67. package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
  68. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  69. package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
  70. package/dist/templates/health-assistant/agent.test.ts +22 -4
  71. package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
  72. package/dist/templates/infocom-adventure/agent.test.ts +53 -24
  73. package/dist/templates/infocom-adventure/agent.ts +21 -0
  74. package/dist/templates/infocom-adventure/client.tsx +246 -164
  75. package/dist/templates/infocom-adventure/shared.ts +20 -9
  76. package/dist/templates/infocom-adventure/system-prompt.md +1 -1
  77. package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
  78. package/dist/templates/link-digest/agent.eval.test.ts +195 -0
  79. package/dist/templates/link-digest/agent.test.ts +24 -19
  80. package/dist/templates/link-digest/client.tsx +47 -61
  81. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  82. package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
  83. package/dist/templates/night-owl/agent.eval.test.ts +272 -0
  84. package/dist/templates/night-owl/agent.test.ts +70 -19
  85. package/dist/templates/night-owl/agent.ts +5 -0
  86. package/dist/templates/night-owl/client.tsx +56 -56
  87. package/dist/templates/night-owl/shared.ts +24 -0
  88. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  89. package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
  90. package/dist/templates/personal-finance/system-prompt.md +4 -0
  91. package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
  92. package/dist/templates/pipeline-simple/agent.ts +2 -2
  93. package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
  94. package/dist/templates/pizza-ordering/agent.test.ts +42 -27
  95. package/dist/templates/pizza-ordering/client.tsx +9 -26
  96. package/dist/templates/pizza-ordering/shared.ts +20 -1
  97. package/dist/templates/pizza-ordering/system-prompt.md +6 -0
  98. package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
  99. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  100. package/dist/templates/plan-and-execute/agent.ts +1 -1
  101. package/dist/templates/plan-and-execute/client.tsx +12 -15
  102. package/dist/templates/plan-and-execute/shared.ts +71 -2
  103. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  104. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  105. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  106. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  107. package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
  108. package/dist/templates/podcast-digest/agent.test.ts +717 -0
  109. package/dist/templates/podcast-digest/agent.ts +139 -0
  110. package/dist/templates/podcast-digest/client.tsx +154 -0
  111. package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
  112. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  113. package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
  114. package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
  115. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  116. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  117. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  118. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  119. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  120. package/dist/templates/redline/agent.eval.test.ts +209 -0
  121. package/dist/templates/redline/agent.test.ts +17 -15
  122. package/dist/templates/redline/client.tsx +12 -12
  123. package/dist/templates/redline/workflows/redline.ts +19 -31
  124. package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
  125. package/dist/templates/research-workflow/agent.test.ts +60 -59
  126. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  127. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  128. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  129. package/dist/templates/retail/address.ts +87 -1
  130. package/dist/templates/retail/agent.eval.test.ts +314 -0
  131. package/dist/templates/retail/agent.test.ts +195 -112
  132. package/dist/templates/retail/agent.ts +35 -1
  133. package/dist/templates/retail/cancel.ts +116 -0
  134. package/dist/templates/retail/client.tsx +265 -117
  135. package/dist/templates/retail/payment.ts +107 -0
  136. package/dist/templates/retail/pending.test.ts +196 -0
  137. package/dist/templates/retail/pending.ts +137 -0
  138. package/dist/templates/retail/registry.test.ts +150 -7
  139. package/dist/templates/retail/returns.ts +102 -0
  140. package/dist/templates/retail/shared.test.ts +42 -0
  141. package/dist/templates/retail/shared.ts +35 -0
  142. package/dist/templates/retail/store.test.ts +84 -15
  143. package/dist/templates/retail/store.ts +231 -48
  144. package/dist/templates/retail/swap.test.ts +4 -4
  145. package/dist/templates/retail/swap.ts +248 -15
  146. package/dist/templates/retail/system-prompt.md +69 -28
  147. package/dist/templates/retail/tools/cancel_change.ts +33 -0
  148. package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
  149. package/dist/templates/retail/tools/confirm_change.ts +42 -0
  150. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
  151. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  152. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  153. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  154. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  155. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  156. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  157. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  158. package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
  159. package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
  160. package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
  161. package/dist/templates/retail/tools/modify_user_address.ts +13 -16
  162. package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
  163. package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
  164. package/dist/templates/simple/agent.eval.test.ts +54 -0
  165. package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
  166. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  167. package/dist/templates/solo-rpg/client.tsx +38 -37
  168. package/dist/templates/solo-rpg/shared.ts +145 -19
  169. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  170. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  171. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  172. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  173. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  174. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  175. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  176. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  177. package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
  178. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  179. package/dist/templates/spoken-summary/agent.ts +30 -17
  180. package/dist/templates/spoken-summary/client.tsx +10 -50
  181. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  182. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  183. package/dist/templates/support-line/agent.eval.test.ts +273 -0
  184. package/dist/templates/support-line/agent.test.ts +11 -16
  185. package/dist/templates/support-line/agent.ts +1 -1
  186. package/dist/templates/support-line/client.tsx +9 -9
  187. package/dist/templates/support-line/nodes.ts +100 -0
  188. package/dist/templates/support-line/procedure.ts +407 -0
  189. package/dist/templates/support-line/system-prompt.md +2 -1
  190. package/dist/templates/support-line/tools/answer_question.ts +66 -4
  191. package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  193. package/dist/templates/transcription-workflow/agent.ts +1 -1
  194. package/dist/templates/transcription-workflow/client.tsx +17 -42
  195. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  196. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  197. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  198. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  199. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  200. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  201. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  202. package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
  203. package/dist/templates/travel-concierge/agent.test.ts +174 -43
  204. package/dist/templates/travel-concierge/agent.ts +18 -9
  205. package/dist/templates/travel-concierge/client.tsx +11 -23
  206. package/dist/templates/travel-concierge/routing.ts +34 -15
  207. package/dist/templates/travel-concierge/shared.ts +180 -12
  208. package/dist/templates/travel-concierge/system-prompt.md +7 -1
  209. package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
  210. package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
  211. package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
  212. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
  213. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
  214. package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
  215. package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
  216. package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
  217. package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
  218. package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
  219. package/dist/templates/web-researcher/system-prompt.md +15 -4
  220. package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
  221. package/dist/test.d.ts +29 -5
  222. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
  223. package/dist/worker-bundler.mjs +1 -1
  224. package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
  225. package/dist/workflow-bundler.d.ts +6 -1
  226. package/dist/workflow.d.ts +1 -1
  227. package/package.json +5 -4
  228. package/dist/eject-31gjtaHF.mjs +0 -116
  229. package/dist/eject.d.ts +0 -60
  230. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  231. package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
  232. package/dist/templates/support-line/graph.ts +0 -224
  233. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  234. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Cancelling a pending order, as a PLAN and an APPLY.
3
+ *
4
+ * The split is what the confirmation gate is built out of, and it is the same
5
+ * split in all seven mutating actions here: `plan*` validates everything and
6
+ * computes the effect without touching the store, `apply*` performs it and
7
+ * cannot fail. A staged plan is what the caller says yes to, so the validation
8
+ * has to happen BEFORE the readback — a "yes" followed by a refusal is the one
9
+ * sequence this gate exists to make impossible.
10
+ */
11
+
12
+ import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
13
+ import { creditRefund, REFUND_DELAY_NOTE, REFUND_IMMEDIATE_NOTE } from "./refund.ts";
14
+ import { resolveOrder } from "./resolve.ts";
15
+ import type { RetailState } from "./shared.ts";
16
+ import { authenticatedUser, money } from "./store.ts";
17
+
18
+ /** The only two reasons the store records. */
19
+ export const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
20
+ export type CancelReason = (typeof CANCEL_REASONS)[number];
21
+
22
+ /**
23
+ * A validated cancellation, holding only storable primitives.
24
+ *
25
+ * No `Order` or `PaymentMethod` reference: a plan is written into the session
26
+ * slot and rides across turns, so a live object here would alias the store and
27
+ * a persisted session could not carry it. Ids and amounts are enough, and
28
+ * looking them up again at apply time is what keeps {@link applyCancel}
29
+ * total.
30
+ */
31
+ export interface CancelPlan {
32
+ /** The sentence the agent reads back before asking for a yes. */
33
+ readBack: string;
34
+ orderId: string;
35
+ reason: CancelReason;
36
+ refunds: { methodId: string; amount: number }[];
37
+ total: number;
38
+ }
39
+
40
+ export function planCancel(
41
+ state: RetailState,
42
+ spokenOrderId: string,
43
+ reason: CancelReason,
44
+ ): CancelPlan | ToolFailure {
45
+ const user = authenticatedUser(state);
46
+ if (isToolFailure(user)) return user;
47
+
48
+ const order = resolveOrder(state, spokenOrderId);
49
+ if (isToolFailure(order)) return order;
50
+
51
+ // The enum makes this unreachable from a well-formed call; it stays because
52
+ // the reason is the one field a caller supplies in free speech, and an LLM
53
+ // tool call arrives untyped.
54
+ if (!CANCEL_REASONS.includes(reason)) {
55
+ return {
56
+ error: `'${reason}' is not an accepted cancellation reason. It must be 'no longer needed' or 'ordered by mistake'.`,
57
+ };
58
+ }
59
+
60
+ // Exactly 'pending'. A 'pending (item modified)' order has spent its one
61
+ // modification and is past cancelling.
62
+ if (order.status !== "pending") {
63
+ return {
64
+ error: `Order ${order.order_id} is ${order.status}, and only a pending order can be cancelled.`,
65
+ };
66
+ }
67
+
68
+ const refunds = order.payment_history
69
+ .filter((payment) => payment.transaction_type === "payment")
70
+ .map((payment) => ({ methodId: payment.payment_method_id, amount: payment.amount }));
71
+ const total = money(refunds.reduce((sum, refund) => sum + refund.amount, 0));
72
+
73
+ const items = order.items.map((item) => item.name).join(", ");
74
+ const destinations = [...new Set(refunds.map((refund) => refund.methodId))].join(" and ");
75
+ return {
76
+ readBack:
77
+ `cancel order ${order.order_id} (${items}) as '${reason}' and refund ` +
78
+ `$${total.toFixed(2)} to ${destinations}`,
79
+ orderId: order.order_id,
80
+ reason,
81
+ refunds,
82
+ total,
83
+ };
84
+ }
85
+
86
+ /** The effect. Total by construction: every id in the plan was resolved from
87
+ * this same store, and nothing between staging and confirming can remove one. */
88
+ export function applyCancel(state: RetailState, plan: CancelPlan) {
89
+ const order = state.store.orders[plan.orderId];
90
+ const user = order ? state.store.users[order.user_id] : undefined;
91
+ let immediate = false;
92
+
93
+ if (order && user) {
94
+ for (const refund of plan.refunds) {
95
+ immediate = creditRefund(user, refund.methodId, refund.amount).immediate || immediate;
96
+ order.payment_history.push({
97
+ transaction_type: "refund",
98
+ amount: refund.amount,
99
+ payment_method_id: refund.methodId,
100
+ });
101
+ }
102
+ order.status = "cancelled";
103
+ order.cancel_reason = plan.reason;
104
+ }
105
+
106
+ return {
107
+ order_id: plan.orderId,
108
+ status: "cancelled" as const,
109
+ cancel_reason: plan.reason,
110
+ refunded: plan.total,
111
+ refund_immediate: immediate,
112
+ message: `Order ${plan.orderId} is cancelled and $${plan.total.toFixed(2)} is being refunded. ${
113
+ immediate ? REFUND_IMMEDIATE_NOTE : REFUND_DELAY_NOTE
114
+ }`,
115
+ };
116
+ }
@@ -1,17 +1,19 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
2
+ import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
3
3
  import {
4
4
  AutoScroll,
5
5
  client,
6
6
  useAgentState,
7
+ useConversation,
7
8
  useSession,
8
- useUserTranscript,
9
+ useSessionSelector,
9
10
  } from "@alexkroman1/aai-ui";
10
11
  import type { ReactNode } from "react";
11
12
  import type {
12
13
  OrderStatus,
13
14
  OrderView,
14
15
  PaymentMethodView,
16
+ PendingView,
15
17
  StoreView,
16
18
  SwapOptionView,
17
19
  } from "./shared.ts";
@@ -154,6 +156,42 @@ function OrderCard({ order, focused }: { order: OrderView; focused: boolean }) {
154
156
 
155
157
  /** Character select. Rendered before authentication, because a user with no
156
158
  * email to give cannot start the conversation at all. */
159
+ /**
160
+ * The change waiting on the caller's word.
161
+ *
162
+ * The one panel that renders BECAUSE nothing has happened. A staged change is
163
+ * the agent's promise about what it is going to do, so showing it beside the
164
+ * orders it has not touched is what lets a watcher catch a readback that does
165
+ * not match the request — which is the failure the whole gate is aimed at, and
166
+ * the one nobody can see from a transcript alone.
167
+ */
168
+ function PendingChange({ pending }: { pending: PendingView }) {
169
+ return (
170
+ <div
171
+ className="rounded-lg p-3"
172
+ style={{
173
+ background: "#fffbeb",
174
+ border: "1px solid #fcd34d",
175
+ animation: "rt-slide-in 180ms ease-out",
176
+ }}
177
+ >
178
+ <div
179
+ className="text-[10px] font-semibold uppercase tracking-wider mb-1.5 flex items-center gap-1.5"
180
+ style={{ color: "#b45309" }}
181
+ >
182
+ <span style={{ animation: "rt-pulse 1.4s ease-in-out infinite" }}>●</span>
183
+ Awaiting the caller's yes
184
+ </div>
185
+ <div className="text-xs leading-relaxed" style={{ color: "#78350f" }}>
186
+ {pending.readBack}
187
+ </div>
188
+ <div className="text-[10px] mt-1.5" style={{ color: "#a16207" }}>
189
+ Nothing has changed yet — {pending.kind}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
194
+
157
195
  function PersonaList() {
158
196
  return (
159
197
  <div className="flex flex-col gap-2">
@@ -217,13 +255,226 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
217
255
  );
218
256
  }
219
257
 
220
- function App() {
258
+ /** One line of the call: a bubble, or the tool the agent ran after it. */
259
+ function Row({ item }: { item: ConversationItem }) {
260
+ if (item.kind === "tool") {
261
+ const { name, status } = item.toolCall;
262
+ return (
263
+ <div
264
+ className="self-start rounded-md px-2.5 py-1 text-[11px] font-mono flex items-center gap-2"
265
+ style={{
266
+ background: "#ffffff",
267
+ border: "1px solid #e4e4e7",
268
+ color: status === "pending" ? "#b45309" : "#71717a",
269
+ }}
270
+ >
271
+ <span
272
+ className="w-1.5 h-1.5 rounded-full inline-block"
273
+ style={{
274
+ background: status === "pending" ? "#ca8a04" : "#16a34a",
275
+ animation: status === "pending" ? "rt-pulse 1s ease-in-out infinite" : "none",
276
+ }}
277
+ />
278
+ {name}
279
+ </div>
280
+ );
281
+ }
282
+ const { role, content } = item.message;
283
+ return (
284
+ <div
285
+ className="rounded-lg text-[13px] max-w-[80%] px-3.5 py-2.5"
286
+ style={{
287
+ lineHeight: 1.55,
288
+ alignSelf: role === "assistant" ? "flex-start" : "flex-end",
289
+ background: role === "assistant" ? "#ffffff" : "#2563eb",
290
+ color: role === "assistant" ? "#18181b" : "#ffffff",
291
+ border: role === "assistant" ? "1px solid #e4e4e7" : "none",
292
+ animation: "rt-slide-in 0.2s ease-out",
293
+ }}
294
+ >
295
+ {content}
296
+ </div>
297
+ );
298
+ }
299
+
300
+ /**
301
+ * The call transcript.
302
+ *
303
+ * `useConversation()` rather than `session.messages.map(...)`. The messages
304
+ * were only part of it: this agent runs FIFTEEN tools and the operator could
305
+ * see none of them, because tool calls live in a second array this page never
306
+ * read — and the streaming reply and the thinking indicator were dropped along
307
+ * with them. The hook owns the interleave (a tool row follows the message it
308
+ * was anchored to), the `null`-vs-`""` transcript distinction and the
309
+ * thinking-suppression rule; the markup below is what this template is for.
310
+ *
311
+ * It subscribes per FIELD, so the customer file in the sidebar no longer
312
+ * re-renders on every partial transcript the way the whole-page `useSession()`
313
+ * this replaced made it.
314
+ */
315
+ function Conversation() {
316
+ const { items, streaming, transcript, thinking } = useConversation();
317
+ return (
318
+ <>
319
+ <AutoScroll
320
+ scrollClassName="rt-scroll overflow-y-auto"
321
+ contentClassName="p-4 flex flex-col gap-2"
322
+ >
323
+ {items.length === 0 && streaming === null && (
324
+ <div className="text-center p-10 text-[13px]" style={{ color: "#a1a1aa" }}>
325
+ Press start, then read one of the emails from “Who to be” to the agent.
326
+ </div>
327
+ )}
328
+ {items.map((item) => (
329
+ <Row
330
+ key={item.kind === "message" ? `m${item.message.id}` : item.toolCall.callId}
331
+ item={item}
332
+ />
333
+ ))}
334
+ {streaming !== null && (
335
+ <div
336
+ className="rounded-lg text-[13px] max-w-[80%] px-3.5 py-2.5 self-start"
337
+ style={{
338
+ lineHeight: 1.55,
339
+ background: "#ffffff",
340
+ color: "#18181b",
341
+ border: "1px solid #e4e4e7",
342
+ }}
343
+ >
344
+ {streaming}
345
+ </div>
346
+ )}
347
+ {thinking && (
348
+ <div className="self-start text-[11px] px-3.5" style={{ color: "#a1a1aa" }}>
349
+ <span style={{ animation: "rt-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
350
+ </div>
351
+ )}
352
+ </AutoScroll>
353
+
354
+ {transcript.speaking && (
355
+ <div
356
+ className="flex items-center px-4 py-2 text-xs italic min-h-8"
357
+ style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
358
+ >
359
+ <span
360
+ className="w-2 h-2 rounded-full inline-block mr-2"
361
+ style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
362
+ />
363
+ {transcript.text}
364
+ </div>
365
+ )}
366
+ </>
367
+ );
368
+ }
369
+
370
+ /** The live status dot, on its own subscription rather than a field off a
371
+ * whole-page read — the header is the only thing here that wants it. */
372
+ function StatusReadout() {
373
+ const state = useSessionSelector((s) => s.state);
374
+ return (
375
+ <>
376
+ <span
377
+ className="w-2 h-2 rounded-full inline-block"
378
+ style={{
379
+ background: STATE_COLORS[state],
380
+ animation:
381
+ state === "listening"
382
+ ? "rt-pulse 1.5s ease-in-out infinite"
383
+ : state === "thinking"
384
+ ? "rt-pulse 0.8s ease-in-out infinite"
385
+ : "none",
386
+ }}
387
+ title={state}
388
+ />
389
+ <span className="text-[11px] font-normal" style={{ color: "#a1a1aa" }}>
390
+ {state}
391
+ </span>
392
+ </>
393
+ );
394
+ }
395
+
396
+ /**
397
+ * The fatal-error banner.
398
+ *
399
+ * `role="alert"` is the part a hand-rolled banner leaves out and the part that
400
+ * matters: per the `fatalError` latch in the session core this is the only
401
+ * remaining signal — the status readout beside it goes back to reading like a
402
+ * live call — and a screen reader is never told an unannounced one appeared.
403
+ * `ConsoleShell` carries it, and this template's two-pane page cannot use that
404
+ * shell as its frame; the attribute is what it is here for.
405
+ */
406
+ function ErrorBanner() {
407
+ const error = useSessionSelector((s) => s.error);
408
+ if (!error) return null;
409
+ return (
410
+ <div
411
+ role="alert"
412
+ className="px-4 py-2 text-xs"
413
+ style={{ background: "#fef2f2", color: "#b91c1c", borderTop: "1px solid #fecaca" }}
414
+ >
415
+ {error.message} ({error.code})
416
+ </div>
417
+ );
418
+ }
419
+
420
+ /** The call controls: the one place a whole-session read is what is wanted —
421
+ * `started`, `running` and three methods, in three buttons. */
422
+ function CallControls({ productCount }: { productCount: number }) {
221
423
  const session = useSession();
222
- // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
223
- // is silence and `""` is speech detected with no words back yet, and reading
224
- // them as one falsy value hides the indicator at the start of every turn.
225
- const { speaking, text: userTranscript } = useUserTranscript();
226
- // The agent's own store, projected by `syncState` after every tool call.
424
+ return (
425
+ <div
426
+ className="flex items-center gap-2 px-4 py-3"
427
+ style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7" }}
428
+ >
429
+ {!session.started ? (
430
+ <button
431
+ type="button"
432
+ className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
433
+ style={{ background: "#2563eb" }}
434
+ onClick={() => session.start()}
435
+ >
436
+ Start call
437
+ </button>
438
+ ) : (
439
+ <>
440
+ <button
441
+ type="button"
442
+ className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer"
443
+ style={{
444
+ background: session.running ? "#e4e4e7" : "#2563eb",
445
+ color: session.running ? "#18181b" : "#ffffff",
446
+ }}
447
+ onClick={() => session.toggle()}
448
+ >
449
+ {session.running ? "Hold" : "Resume"}
450
+ </button>
451
+ {/* end() hangs up and flips `started` back, so the UI
452
+ returns to "Start call" and the next start is a brand-new
453
+ session (fresh store, greeting included). reset() would
454
+ keep the call live — the buttons never toggle back. */}
455
+ <button
456
+ type="button"
457
+ className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
458
+ style={{ background: "#dc2626" }}
459
+ onClick={() => session.end()}
460
+ >
461
+ End
462
+ </button>
463
+ </>
464
+ )}
465
+ <div className="flex-1" />
466
+ <span className="text-[10px] tabular-nums" style={{ color: "#a1a1aa" }}>
467
+ {productCount} products
468
+ </span>
469
+ </div>
470
+ );
471
+ }
472
+
473
+ function App() {
474
+ // The agent's own store, projected by `syncState` after every tool call —
475
+ // the only subscription at this level now. The session reads that used to sit
476
+ // beside it moved into the four components above, so a partial transcript no
477
+ // longer re-renders the customer file.
227
478
  const view = useAgentState<StoreView>(EMPTY_VIEW);
228
479
 
229
480
  const lastAction = view.activity.at(-1);
@@ -242,22 +493,7 @@ function App() {
242
493
  <div className="flex items-center gap-2.5 text-base font-semibold">
243
494
  <span style={{ color: "#2563eb" }}>◆</span>
244
495
  Retail Support
245
- <span
246
- className="w-2 h-2 rounded-full inline-block"
247
- style={{
248
- background: STATE_COLORS[session.state],
249
- animation:
250
- session.state === "listening"
251
- ? "rt-pulse 1.5s ease-in-out infinite"
252
- : session.state === "thinking"
253
- ? "rt-pulse 0.8s ease-in-out infinite"
254
- : "none",
255
- }}
256
- title={session.state}
257
- />
258
- <span className="text-[11px] font-normal" style={{ color: "#a1a1aa" }}>
259
- {session.state}
260
- </span>
496
+ <StatusReadout />
261
497
  </div>
262
498
  <span
263
499
  className="px-2.5 py-1 rounded text-[10px] font-semibold uppercase tracking-wider"
@@ -280,99 +516,9 @@ function App() {
280
516
  className="flex flex-col overflow-hidden"
281
517
  style={{ borderRight: "1px solid #e4e4e7" }}
282
518
  >
283
- <AutoScroll
284
- scrollClassName="rt-scroll overflow-y-auto"
285
- contentClassName="p-4 flex flex-col gap-2"
286
- >
287
- {session.messages.length === 0 && (
288
- <div className="text-center p-10 text-[13px]" style={{ color: "#a1a1aa" }}>
289
- Press start, then read one of the emails from “Who to be” to the agent.
290
- </div>
291
- )}
292
- {session.messages.map((message: ChatMessage, index: number) => (
293
- <div
294
- key={index}
295
- className="rounded-lg text-[13px] max-w-[80%] px-3.5 py-2.5"
296
- style={{
297
- lineHeight: 1.55,
298
- alignSelf: message.role === "assistant" ? "flex-start" : "flex-end",
299
- background: message.role === "assistant" ? "#ffffff" : "#2563eb",
300
- color: message.role === "assistant" ? "#18181b" : "#ffffff",
301
- border: message.role === "assistant" ? "1px solid #e4e4e7" : "none",
302
- animation: "rt-slide-in 0.2s ease-out",
303
- }}
304
- >
305
- {message.content}
306
- </div>
307
- ))}
308
- </AutoScroll>
309
-
310
- {speaking && (
311
- <div
312
- className="flex items-center px-4 py-2 text-xs italic min-h-8"
313
- style={{ background: "#fafafa", borderTop: "1px solid #e4e4e7", color: "#71717a" }}
314
- >
315
- <span
316
- className="w-2 h-2 rounded-full inline-block mr-2"
317
- style={{ background: "#16a34a", animation: "rt-pulse 1.5s ease-in-out infinite" }}
318
- />
319
- {userTranscript}
320
- </div>
321
- )}
322
- {session.error && (
323
- <div
324
- className="px-4 py-2 text-xs"
325
- style={{ background: "#fef2f2", color: "#b91c1c", borderTop: "1px solid #fecaca" }}
326
- >
327
- {session.error.message} ({session.error.code})
328
- </div>
329
- )}
330
-
331
- <div
332
- className="flex items-center gap-2 px-4 py-3"
333
- style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7" }}
334
- >
335
- {!session.started ? (
336
- <button
337
- type="button"
338
- className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
339
- style={{ background: "#2563eb" }}
340
- onClick={() => session.start()}
341
- >
342
- Start call
343
- </button>
344
- ) : (
345
- <>
346
- <button
347
- type="button"
348
- className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer"
349
- style={{
350
- background: session.running ? "#e4e4e7" : "#2563eb",
351
- color: session.running ? "#18181b" : "#ffffff",
352
- }}
353
- onClick={() => session.toggle()}
354
- >
355
- {session.running ? "Hold" : "Resume"}
356
- </button>
357
- {/* end() hangs up and flips `started` back, so the UI
358
- returns to "Start call" and the next start is a brand-new
359
- session (fresh store, greeting included). reset() would
360
- keep the call live — the buttons never toggle back. */}
361
- <button
362
- type="button"
363
- className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
364
- style={{ background: "#dc2626" }}
365
- onClick={() => session.end()}
366
- >
367
- End
368
- </button>
369
- </>
370
- )}
371
- <div className="flex-1" />
372
- <span className="text-[10px] tabular-nums" style={{ color: "#a1a1aa" }}>
373
- {view.productCount} products
374
- </span>
375
- </div>
519
+ <Conversation />
520
+ <ErrorBanner />
521
+ <CallControls productCount={view.productCount} />
376
522
  </div>
377
523
 
378
524
  {/* Sidebar: the customer file */}
@@ -380,6 +526,8 @@ function App() {
380
526
  className="rt-scroll overflow-y-auto p-3 flex flex-col gap-3"
381
527
  style={{ background: "#f4f4f5" }}
382
528
  >
529
+ {view.pending && <PendingChange pending={view.pending} />}
530
+
383
531
  {!view.customer && (
384
532
  <Panel title="Who to be">
385
533
  <PersonaList />
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Changing which payment method a pending order is charged to, as a plan and an
3
+ * apply (see `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, money } from "./store.ts";
10
+
11
+ export interface PaymentPlan {
12
+ readBack: string;
13
+ orderId: string;
14
+ /** Where the order is charged after the change. */
15
+ newMethodId: string;
16
+ /** Where the original charge goes back to. */
17
+ oldMethodId: string;
18
+ amount: number;
19
+ }
20
+
21
+ export function planPayment(
22
+ state: RetailState,
23
+ spokenOrderId: string,
24
+ newMethodId: string,
25
+ ): PaymentPlan | ToolFailure {
26
+ const user = authenticatedUser(state);
27
+ if (isToolFailure(user)) return user;
28
+
29
+ const order = resolveOrder(state, spokenOrderId);
30
+ if (isToolFailure(order)) return order;
31
+
32
+ if (!order.status.startsWith("pending")) {
33
+ return {
34
+ error: `Order ${order.order_id} is ${order.status}, and only a pending order's payment method can be changed.`,
35
+ };
36
+ }
37
+
38
+ const newMethod = findPaymentMethod(user, newMethodId);
39
+ if (isToolFailure(newMethod)) return newMethod;
40
+
41
+ const original = order.payment_history[0];
42
+ if (order.payment_history.length !== 1 || original?.transaction_type !== "payment") {
43
+ return {
44
+ error: `Order ${order.order_id} does not have exactly one payment on record, so its payment method cannot be changed.`,
45
+ };
46
+ }
47
+ if (original.payment_method_id === newMethodId) {
48
+ return {
49
+ error: `Order ${order.order_id} is already paid with ${newMethodId}. The new method must be different.`,
50
+ };
51
+ }
52
+
53
+ const amount = original.amount;
54
+ if (isGiftCard(newMethod) && newMethod.balance < amount) {
55
+ return {
56
+ error: `Gift card ${newMethodId}'s balance ($${newMethod.balance.toFixed(2)}) does not cover the $${amount.toFixed(2)} order total.`,
57
+ };
58
+ }
59
+
60
+ return {
61
+ readBack:
62
+ `charge order ${order.order_id} — $${amount.toFixed(2)} — to ${newMethodId} instead, ` +
63
+ `refunding ${original.payment_method_id}`,
64
+ orderId: order.order_id,
65
+ newMethodId,
66
+ oldMethodId: original.payment_method_id,
67
+ amount,
68
+ };
69
+ }
70
+
71
+ export function applyPayment(state: RetailState, plan: PaymentPlan) {
72
+ const order = state.store.orders[plan.orderId];
73
+ const user = order ? state.store.users[order.user_id] : undefined;
74
+
75
+ if (order && user) {
76
+ order.payment_history.push(
77
+ {
78
+ transaction_type: "payment",
79
+ amount: plan.amount,
80
+ payment_method_id: plan.newMethodId,
81
+ },
82
+ {
83
+ transaction_type: "refund",
84
+ amount: plan.amount,
85
+ payment_method_id: plan.oldMethodId,
86
+ },
87
+ );
88
+
89
+ const newMethod = user.payment_methods[plan.newMethodId];
90
+ if (newMethod && isGiftCard(newMethod)) {
91
+ newMethod.balance = money(newMethod.balance - plan.amount);
92
+ }
93
+ const oldMethod = user.payment_methods[plan.oldMethodId];
94
+ if (oldMethod && isGiftCard(oldMethod)) {
95
+ oldMethod.balance = money(oldMethod.balance + plan.amount);
96
+ }
97
+ }
98
+
99
+ return {
100
+ order_id: plan.orderId,
101
+ status: order?.status ?? "pending",
102
+ amount: plan.amount,
103
+ paid_with: plan.newMethodId,
104
+ refunded_to: plan.oldMethodId,
105
+ message: `Order ${plan.orderId} is now charged to ${plan.newMethodId}, and $${plan.amount.toFixed(2)} is going back to ${plan.oldMethodId}.`,
106
+ };
107
+ }