@alexkroman1/aai-cli 6.10.1 → 7.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 (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +93 -14
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +13 -4
  28. package/dist/scaffold/vite.config.ts +1 -1
  29. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  30. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  31. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  32. package/dist/templates/call-audit/agent.test.ts +818 -0
  33. package/dist/templates/call-audit/agent.ts +171 -0
  34. package/dist/templates/call-audit/client.tsx +198 -0
  35. package/dist/templates/call-audit/workflows/audit.ts +298 -0
  36. package/dist/templates/call-audit/workflows/ingest.ts +251 -0
  37. package/dist/templates/call-audit/workflows/media.ts +637 -0
  38. package/dist/templates/call-audit/workflows/summarize.ts +201 -0
  39. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  40. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  41. package/dist/templates/dispatch-center/client.tsx +239 -129
  42. package/dist/templates/dispatch-center/shared.ts +99 -1
  43. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  44. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  45. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  46. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  47. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  48. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  49. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  50. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  51. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  52. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  53. package/dist/templates/health-assistant/agent.test.ts +22 -4
  54. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  55. package/dist/templates/infocom-adventure/client.tsx +246 -164
  56. package/dist/templates/link-digest/agent.test.ts +24 -19
  57. package/dist/templates/link-digest/client.tsx +47 -61
  58. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  59. package/dist/templates/night-owl/agent.test.ts +70 -19
  60. package/dist/templates/night-owl/agent.ts +5 -0
  61. package/dist/templates/night-owl/client.tsx +56 -56
  62. package/dist/templates/night-owl/shared.ts +24 -0
  63. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  64. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  65. package/dist/templates/pizza-ordering/client.tsx +9 -26
  66. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  67. package/dist/templates/plan-and-execute/agent.ts +1 -1
  68. package/dist/templates/plan-and-execute/client.tsx +12 -15
  69. package/dist/templates/plan-and-execute/shared.ts +71 -2
  70. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  71. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  72. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  73. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  74. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  75. package/dist/templates/podcast-digest/agent.ts +139 -0
  76. package/dist/templates/podcast-digest/client.tsx +154 -0
  77. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  78. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  79. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  80. package/dist/templates/recap-workflow/agent.test.ts +107 -96
  81. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  82. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  83. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  84. package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
  85. package/dist/templates/redline/agent.test.ts +17 -15
  86. package/dist/templates/redline/client.tsx +12 -12
  87. package/dist/templates/redline/workflows/redline.ts +19 -31
  88. package/dist/templates/research-workflow/agent.test.ts +60 -59
  89. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  90. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  91. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  92. package/dist/templates/retail/agent.test.ts +26 -23
  93. package/dist/templates/retail/client.tsx +226 -117
  94. package/dist/templates/retail/registry.test.ts +38 -6
  95. package/dist/templates/retail/store.test.ts +82 -15
  96. package/dist/templates/retail/store.ts +174 -47
  97. package/dist/templates/retail/system-prompt.md +11 -2
  98. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  99. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  100. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  101. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  102. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  105. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  106. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  107. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  108. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  109. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  110. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  111. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  112. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  113. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  114. package/dist/templates/solo-rpg/client.tsx +38 -37
  115. package/dist/templates/solo-rpg/shared.ts +145 -19
  116. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  117. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  118. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  119. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  120. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  121. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  122. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  123. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  124. package/dist/templates/spoken-summary/agent.test.ts +314 -0
  125. package/dist/templates/spoken-summary/agent.ts +155 -0
  126. package/dist/templates/spoken-summary/client.tsx +185 -0
  127. package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
  128. package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
  129. package/dist/templates/support-line/agent.test.ts +11 -16
  130. package/dist/templates/support-line/agent.ts +1 -1
  131. package/dist/templates/support-line/client.tsx +9 -9
  132. package/dist/templates/support-line/nodes.ts +100 -0
  133. package/dist/templates/support-line/procedure.ts +407 -0
  134. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  135. package/dist/templates/transcription-workflow/agent.test.ts +246 -123
  136. package/dist/templates/transcription-workflow/agent.ts +21 -7
  137. package/dist/templates/transcription-workflow/client.tsx +17 -42
  138. package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
  139. package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
  140. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  141. package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
  142. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  143. package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
  144. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
  145. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  146. package/dist/templates/travel-concierge/client.tsx +11 -23
  147. package/dist/templates/travel-concierge/routing.ts +34 -15
  148. package/dist/templates/travel-concierge/shared.ts +70 -3
  149. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  153. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  154. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  155. package/dist/worker-bundler.mjs +1 -1
  156. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  157. package/dist/workflow-bundler.d.ts +6 -1
  158. package/dist/workflow.d.ts +1 -1
  159. package/package.json +5 -4
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { ToolContext, ToolFailure } from "@alexkroman1/aai";
2
- import { isToolFailure, pushCapped, sessionSlot, toolFailure } from "@alexkroman1/aai";
2
+ import { dialog, isToolFailure, omitUndefined, pushCapped, sessionSlot } from "@alexkroman1/aai";
3
3
  import type { z } from "zod";
4
4
  import seedJson from "./seed.json";
5
5
  import type {
@@ -71,6 +71,89 @@ export function seedStore(): Store {
71
71
  */
72
72
  export const retailSlot = sessionSlot("retail", createDefaultState);
73
73
 
74
+ // ─── The call, as a machine ──────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Where this call is, and what may be done from here, as a plain state map.
78
+ *
79
+ * A {@link DialogSpec} rather than an XState machine: this dialog said three
80
+ * states, two events and an instruction each, which is exactly what a spec can
81
+ * say — and the `setup({ types: {} as { events: … } })` block it used to carry
82
+ * restated the two event names already written in the `on` maps. The
83
+ * instruction is a declared field now instead of an untyped `meta` bag, so
84
+ * misspelling it is a compile error rather than a refusal that arrives with no
85
+ * recovery text. `as const` is what keeps the `on` keys literal, so `send`
86
+ * below is checked against the events this spec actually declares.
87
+ *
88
+ * The policy's first two sections — "Authenticate first" and "Handing off to a
89
+ * human" — used to be prose plus a boolean. `requiresAuth` was that boolean:
90
+ * fifteen tools declared it (five opting out), the wrapper below read it, and a
91
+ * refusal answered one fixed sentence. Three things change by declaring the
92
+ * states instead.
93
+ *
94
+ * **`transferred` is a real terminal state, and it was not enforced at all.**
95
+ * The policy says to call `transfer_to_human_agents` and then say exactly one
96
+ * sentence "and nothing else" — which nothing checked, so every tool stayed
97
+ * callable after the handoff and a model that kept going would keep acting on a
98
+ * call it had already given away. It is a `final` state now: no tool declares
99
+ * itself legal there, so every one of them refuses.
100
+ *
101
+ * **The refusal quotes the state.** `when` names where a tool may run and the
102
+ * SDK writes the sentence, so the message is one thing rather than a constant
103
+ * threaded through a wrapper — and it says where the call actually is, which
104
+ * "Not authenticated" could not.
105
+ *
106
+ * **The position rides every result.** A flow tool answers the author's value
107
+ * wrapped in the position it landed in, so the stage and its instruction reach
108
+ * the model on every call rather than only when the prompt is still in context.
109
+ *
110
+ * `IDENTIFIED` is declared on `serving` as well, because a caller repeating
111
+ * their email must not hit an error — see `authenticateAs`, which is what
112
+ * refuses a switch to a DIFFERENT customer. Whether the flow is identified and
113
+ * WHO it is identified as are two facts: this holds the first,
114
+ * `authenticatedUserId` holds the second.
115
+ */
116
+ const callSpec = {
117
+ initial: "identifying",
118
+ states: {
119
+ identifying: {
120
+ // The instruction NAMES the two tools, because this sentence is what a
121
+ // refusal quotes and a refusal is the model's recovery path — the same
122
+ // job the removed `NOT_AUTHENTICATED` constant did, now attached to the
123
+ // state that means it.
124
+ instruction:
125
+ "You do not know who this is yet. Identify the caller with " +
126
+ "find_user_id_by_email, or find_user_id_by_name_zip if they cannot " +
127
+ "remember the email. Do this even if they volunteer a user id.",
128
+ on: { IDENTIFIED: "serving", TRANSFERRED: "transferred" },
129
+ },
130
+ serving: {
131
+ instruction:
132
+ "You are helping one identified customer, and only that one. Say what you " +
133
+ "are about to change — the order, the items, the amounts, where the money " +
134
+ "goes — and wait for an explicit yes before you call anything that changes it.",
135
+ on: { IDENTIFIED: "serving", TRANSFERRED: "transferred" },
136
+ },
137
+ transferred: {
138
+ final: true,
139
+ instruction:
140
+ "The call belongs to a human agent now. Say nothing beyond the transfer " +
141
+ "sentence, and do nothing else.",
142
+ },
143
+ },
144
+ } as const;
145
+
146
+ /**
147
+ * The flow. Its own slot key beside {@link retailSlot}: the flow holds the
148
+ * POSITION and the store holds the customer, the orders and the activity feed.
149
+ */
150
+ export const callFlow = dialog("call", callSpec);
151
+
152
+ /** Every state a tool may run in before the call is handed to a human — i.e.
153
+ * everything but `transferred`. What the five formerly `requiresAuth: false`
154
+ * tools declare, so the terminal state gates them without an auth gate. */
155
+ export const BEFORE_TRANSFER = ["identifying", "serving"] as const;
156
+
74
157
  export function setFocus(
75
158
  state: RetailState,
76
159
  focus: { orderId?: string; productId?: string },
@@ -128,12 +211,26 @@ export function findPaymentMethod(user: User, methodId: string): PaymentMethod |
128
211
 
129
212
  // ─── Guards ──────────────────────────────────────────────────────────────────
130
213
 
131
- const NOT_AUTHENTICATED =
132
- "Not authenticated. Identify the customer first with find_user_id_by_email, " +
133
- "or find_user_id_by_name_zip if they cannot remember their email.";
134
-
214
+ /**
215
+ * The customer on this call.
216
+ *
217
+ * The null arm is reachable only if the POSITION and the STORE disagree — the
218
+ * flow says `serving` while nothing latched a user id — which no code path
219
+ * produces, since `authenticateAs` is what both writes the id and lets the
220
+ * `IDENTIFIED` event through. It is kept and reported rather than thrown for the
221
+ * reason `travel-concierge`'s `cancel_action` keeps its own: this runs mid-call,
222
+ * and a sentence the model can act on beats an exception. The GATE that a
223
+ * customer is identified at all is `callFlow`'s, declared per tool as `when`.
224
+ */
135
225
  export function authenticatedUser(state: RetailState): User | ToolFailure {
136
- if (!state.authenticatedUserId) return { error: NOT_AUTHENTICATED };
226
+ if (!state.authenticatedUserId) {
227
+ return {
228
+ error:
229
+ "No customer is latched onto this call yet. Identify them with " +
230
+ "find_user_id_by_email, or find_user_id_by_name_zip if they cannot " +
231
+ "remember their email.",
232
+ };
233
+ }
137
234
  return findUser(state, state.authenticatedUserId);
138
235
  }
139
236
 
@@ -187,24 +284,42 @@ interface RetailToolSpec<S extends z.ZodType<Record<string, unknown>>, R> {
187
284
  /** Required even for no-arg tools — pass `z.object({})`. One code path in the
188
285
  * wrapper is worth more than saving a line at one call site. */
189
286
  inputSchema: S;
190
- /** Default true. Only the two finder tools and the three catalog tools opt
191
- * out; everything else touches customer data. */
192
- requiresAuth?: boolean;
193
- summary: (args: z.output<S>, result: R) => string;
194
287
  /**
195
- * Handed the store as its second argument, and SYNCHRONOUS.
288
+ * The state(s) this tool may run in, as `callFlow`'s states spell them.
196
289
  *
197
- * **Declare it BEFORE `summary` in the object literal.** TS infers this
198
- * wrapper's generic `R` from `execute`'s return type and processes an object
199
- * literal's properties in SOURCE ORDER, so with `summary` written first its
200
- * `result` parameter has nothing to infer from and silently falls back to
201
- * `unknown` every `isToolFailure(result) ? : result.order_id` in the
202
- * fifteen tool files then stops compiling, or worse, stops meaning anything.
203
- * It lives here rather than in each tool file because it is a property of
204
- * this type: the same four lines were pasted into eight of the fifteen and
205
- * pointed at from five more, which is a rule maintained in fourteen places.
290
+ * Replaces the `requiresAuth` boolean this spec used to carry. Ten tools want
291
+ * `"serving"`; the two finders, the three catalog reads and the transfer want
292
+ * {@link BEFORE_TRANSFER}, which is every state but the terminal one — so
293
+ * "does not need a customer" and "is still legal after the handoff" stopped
294
+ * being the same claim, and they were never the same claim.
295
+ */
296
+ when: string | readonly string[];
297
+ /** The event to send once the body has succeeded, for a tool that MOVES the
298
+ * call. Three do: the two finders send `IDENTIFIED`, the transfer sends
299
+ * `TRANSFERRED`. Nothing is sent when the body answers a `ToolFailure`. */
300
+ send?: { type: "IDENTIFIED" } | { type: "TRANSFERRED" };
301
+ /**
302
+ * One line for the activity feed, from the call that SUCCEEDED.
303
+ *
304
+ * `R` is the SUCCESS type — `execute` below is declared `R | ToolFailure`, so
305
+ * the failure arm is matched against `ToolFailure` during inference and never
306
+ * lands in `R`. That is what lets the fifteen tool files stop writing
307
+ * `isToolFailure(result) ? "… failed" : …` for a case that cannot arrive: the
308
+ * wrapper never calls `summary` on a failure and now says so in the type.
206
309
  *
207
- * ---
310
+ * `NoInfer` takes this parameter out of the inference race `R` used to be
311
+ * decided by. It does NOT make the source order irrelevant — `execute` is an
312
+ * inline arrow whose parameters are contextually typed, so its return type is
313
+ * inferred in a LATER pass than this signature is checked, and a `summary`
314
+ * written above it still lands on `unknown`. What changed is that the failure
315
+ * is now LOUD: with no `isToolFailure` ternary left to swallow it, the wrong
316
+ * order is a `TS18046` on the first property read rather than a narrowing
317
+ * that quietly stops meaning anything. So the twelve-line warning this used
318
+ * to carry is the compiler's job now.
319
+ */
320
+ summary: (args: z.output<S>, result: NoInfer<R>) => string;
321
+ /**
322
+ * Handed the store as its second argument, and SYNCHRONOUS.
208
323
  *
209
324
  * **The draft is passed in rather than re-read**, which is the one change the
210
325
  * durable store forced on this template. The body used to open with
@@ -218,7 +333,7 @@ interface RetailToolSpec<S extends z.ZodType<Record<string, unknown>>, R> {
218
333
  * so none of them wants to; a tool that DID would await outside the wrapper and
219
334
  * call `retailSlot.update` itself, the way `plan-and-execute`'s do.
220
335
  */
221
- execute: (args: z.output<S>, state: RetailState, ctx: ToolContext) => R;
336
+ execute: (args: z.output<S>, state: RetailState, ctx: ToolContext) => R | ToolFailure;
222
337
  }
223
338
 
224
339
  function record(state: RetailState, name: string, summary: string): void {
@@ -231,13 +346,26 @@ function record(state: RetailState, name: string, summary: string): void {
231
346
  }
232
347
 
233
348
  /**
234
- * Every retail tool is built through this. It owns three things no tool body
235
- * may re-implement:
349
+ * Every retail tool is built through this. It owns two things no tool body may
350
+ * re-implement:
351
+ *
352
+ * 1. the mutation window the body's draft comes from,
353
+ * 2. the `callSeq` increment + activity entry — the reason the UI moves on
354
+ * every tool call rather than only when a projected value happens to differ.
236
355
  *
237
- * 1. the authentication gate,
238
- * 2. serialization of the state mutation,
239
- * 3. the `callSeq` increment + activity entry the reason the UI moves on
240
- * EVERY tool call rather than only when a projected value happens to differ.
356
+ * The third thing it used to own — the authentication gate — is
357
+ * {@link callFlow}'s now, declared per tool as `when`. What that buys is in the
358
+ * machine's own doc; what it COSTS is one line of the activity feed: a refused
359
+ * call short-circuits before this wrapper's body runs, so a blocked call no
360
+ * longer records `blocked: not authenticated` and no longer bumps `callSeq`.
361
+ * That is the right trade — the refusal reaches the model, which the sidebar
362
+ * line never did, and it carries the state and its instruction rather than one
363
+ * fixed sentence.
364
+ *
365
+ * **`callFlow.tool` rather than `retailSlot.updateTool`**, so the body opens the
366
+ * store's window itself. A flow tool's own `execute` is handed `(args, ctx)`;
367
+ * everything else about a tool body here is unchanged, including that it is
368
+ * synchronous — the window cannot span an await.
241
369
  *
242
370
  * `focus` is deliberately left to tool bodies (`setFocus`): it is a UI nicety,
243
371
  * not an invariant, and only the body knows what the call was about.
@@ -245,26 +373,25 @@ function record(state: RetailState, name: string, summary: string): void {
245
373
  export function retailTool<S extends z.ZodType<Record<string, unknown>>, R>(
246
374
  spec: RetailToolSpec<S, R>,
247
375
  ) {
248
- const requiresAuth = spec.requiresAuth ?? true;
249
- // `updateTool` rather than `tool` + a hand-written `retailSlot.update`: it runs
250
- // the body inside the slot's mutation window and hands it the draft, so this
251
- // wrapper is left with only what is specific to THIS agent.
252
- return retailSlot.updateTool({
376
+ return callFlow.tool({
253
377
  description: spec.description,
254
378
  inputSchema: spec.inputSchema,
255
- execute: (args, state, ctx) => {
256
- const typedArgs = args as z.output<S>;
257
- if (requiresAuth && !state.authenticatedUserId) {
258
- record(state, spec.name, "blocked: not authenticated");
259
- return toolFailure(NOT_AUTHENTICATED);
260
- }
261
- const result = spec.execute(typedArgs, state, ctx);
262
- record(
263
- state,
264
- spec.name,
265
- isToolFailure(result) ? `error: ${result.error}` : spec.summary(typedArgs, result),
266
- );
267
- return result;
268
- },
379
+ when: spec.when,
380
+ // `omitUndefined` rather than a conditional spread: `exactOptionalPropertyTypes`
381
+ // is on, so an explicit `send: undefined` is not the same as an absent one —
382
+ // and only three of the fifteen tools declare an event (guard-invariants
383
+ // rule 2).
384
+ ...omitUndefined({ send: spec.send }),
385
+ execute: (args, ctx) =>
386
+ retailSlot.update(ctx, (state) => {
387
+ const typedArgs = args as z.output<S>;
388
+ const result = spec.execute(typedArgs, state, ctx);
389
+ record(
390
+ state,
391
+ spec.name,
392
+ isToolFailure(result) ? `error: ${result.error}` : spec.summary(typedArgs, result),
393
+ );
394
+ return result;
395
+ }),
269
396
  });
270
397
  }
@@ -3,12 +3,20 @@ customer on the phone. You can help them cancel or modify pending orders,
3
3
  return or exchange delivered orders, update their default address, and answer
4
4
  questions about their own profile, orders and our products.
5
5
 
6
+ # Where the call is
7
+
8
+ Every tool answers with the stage this call is in and what that stage expects
9
+ next. Read it — it is the shortest true statement of what you may do, and you do
10
+ not have to remember it between turns. A tool that is not available yet refuses
11
+ outright and tells you what has to happen first.
12
+
6
13
  # Authenticate first
7
14
 
8
15
  Before anything else, identify who you are talking to by finding their user id
9
16
  — by email (`find_user_id_by_email`), or by first name, last name and zip code
10
17
  (`find_user_id_by_name_zip`) if they cannot remember the email. Do this **even
11
- if the caller volunteers their user id**.
18
+ if the caller volunteers their user id**. Until you have, every other tool will
19
+ refuse.
12
20
 
13
21
  Prefer email. Fall back to name + zip only when they cannot recall the address
14
22
  on the account.
@@ -45,7 +53,8 @@ speak; when you speak, do not also make a tool call.
45
53
  Transfer only if the caller explicitly asks for a human, or their request
46
54
  cannot be handled with your tools and this policy. Call
47
55
  `transfer_to_human_agents` first, then say exactly: "You are being transferred
48
- to a human agent. Please hold on." Say nothing else after that.
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.
49
58
 
50
59
  # Speaking on the phone
51
60
 
@@ -9,6 +9,7 @@ const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
9
9
 
10
10
  export default retailTool({
11
11
  name: "cancel_pending_order",
12
+ when: "serving",
12
13
  description:
13
14
  "Cancel a pending order. Only an order whose status is exactly 'pending' can be cancelled — " +
14
15
  "check the status first. The reason must be either 'no longer needed' or 'ordered by mistake'. " +
@@ -70,6 +71,5 @@ export default retailTool({
70
71
  }`,
71
72
  };
72
73
  },
73
- summary: (_args, result) =>
74
- isToolFailure(result) ? "cancel failed" : `cancelled ${result.order_id}`,
74
+ summary: (_args, result) => `cancelled ${result.order_id}`,
75
75
  });
@@ -6,6 +6,7 @@ import { assertCanCoverDiff, planItemSwap } from "../swap.ts";
6
6
 
7
7
  export default retailTool({
8
8
  name: "exchange_delivered_order_items",
9
+ when: "serving",
9
10
  description:
10
11
  "Request an exchange of items in a delivered order for different options of the SAME products. " +
11
12
  "Only a 'delivered' order can be exchanged, and only once. Remind the caller to name EVERY " +
@@ -87,6 +88,5 @@ export default retailTool({
87
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.`,
88
89
  };
89
90
  },
90
- summary: (_args, result) =>
91
- isToolFailure(result) ? "exchange failed" : `exchange requested on ${result.order_id}`,
91
+ summary: (_args, result) => `exchange requested on ${result.order_id}`,
92
92
  });
@@ -1,7 +1,6 @@
1
- import { isToolFailure } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import { authenticateAs } from "../authenticate.ts";
4
- import { retailTool } from "../store.ts";
3
+ import { BEFORE_TRANSFER, retailTool } from "../store.ts";
5
4
 
6
5
  export default retailTool({
7
6
  name: "find_user_id_by_email",
@@ -12,7 +11,12 @@ export default retailTool({
12
11
  inputSchema: z.object({
13
12
  email: z.string().max(200).describe("The customer's email, e.g. 'something@example.com'"),
14
13
  }),
15
- requiresAuth: false,
14
+ // Legal in `serving` too: a caller repeating their email must not hit an
15
+ // error, and `authenticateAs` is what refuses a switch to a DIFFERENT
16
+ // customer. The `IDENTIFIED` event is not sent on that refusal — a flow tool
17
+ // sends nothing when its body answers a `ToolFailure`.
18
+ when: BEFORE_TRANSFER,
19
+ send: { type: "IDENTIFIED" },
16
20
  execute: (args, state) => {
17
21
  const target = args.email.trim().toLowerCase();
18
22
  const match = Object.values(state.store.users).find(
@@ -23,6 +27,5 @@ export default retailTool({
23
27
  }
24
28
  return authenticateAs(state, match);
25
29
  },
26
- summary: (_args, result) =>
27
- isToolFailure(result) ? "lookup failed" : `identified ${result.user_id}`,
30
+ summary: (_args, result) => `identified ${result.user_id}`,
28
31
  });
@@ -1,7 +1,6 @@
1
- import { isToolFailure } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
2
  import { authenticateAs } from "../authenticate.ts";
4
- import { retailTool } from "../store.ts";
3
+ import { BEFORE_TRANSFER, retailTool } from "../store.ts";
5
4
 
6
5
  export default retailTool({
7
6
  name: "find_user_id_by_name_zip",
@@ -13,7 +12,8 @@ export default retailTool({
13
12
  last_name: z.string().max(100).describe("Last name, e.g. 'Doe'"),
14
13
  zip: z.string().max(20).describe("Zip code, e.g. '12345'"),
15
14
  }),
16
- requiresAuth: false,
15
+ when: BEFORE_TRANSFER,
16
+ send: { type: "IDENTIFIED" },
17
17
  execute: (args, state) => {
18
18
  const first = args.first_name.trim().toLowerCase();
19
19
  const last = args.last_name.trim().toLowerCase();
@@ -31,6 +31,5 @@ export default retailTool({
31
31
  }
32
32
  return authenticateAs(state, match);
33
33
  },
34
- summary: (_args, result) =>
35
- isToolFailure(result) ? "lookup failed" : `identified ${result.user_id}`,
34
+ summary: (_args, result) => `identified ${result.user_id}`,
36
35
  });
@@ -1,6 +1,6 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { findItem, retailTool } from "../store.ts";
3
+ import { BEFORE_TRANSFER, findItem, retailTool } from "../store.ts";
4
4
 
5
5
  export default retailTool({
6
6
  name: "get_item_details",
@@ -10,7 +10,7 @@ export default retailTool({
10
10
  inputSchema: z.object({
11
11
  item_id: z.string().max(60).describe("The item id, e.g. '1008292230'"),
12
12
  }),
13
- requiresAuth: false,
13
+ when: BEFORE_TRANSFER,
14
14
  execute: (args, state) => {
15
15
  const found = findItem(state, args.item_id);
16
16
  if (isToolFailure(found)) return found;
@@ -23,6 +23,5 @@ export default retailTool({
23
23
  available: found.variant.available,
24
24
  };
25
25
  },
26
- summary: (_args, result) =>
27
- isToolFailure(result) ? "item read failed" : `read item ${result.item_id}`,
26
+ summary: (_args, result) => `read item ${result.item_id}`,
28
27
  });
@@ -1,11 +1,11 @@
1
- import { isToolFailure } from "@alexkroman1/aai";
2
- import { omitUndefined } from "@alexkroman1/aai/utils";
1
+ import { isToolFailure, omitUndefined } from "@alexkroman1/aai";
3
2
  import { z } from "zod";
4
3
  import { OrderIdField, resolveOrder } from "../resolve.ts";
5
4
  import { retailTool, setFocus } from "../store.ts";
6
5
 
7
6
  export default retailTool({
8
7
  name: "get_order_details",
8
+ when: "serving",
9
9
  description:
10
10
  "Get the status and full details of one of the authenticated customer's orders. Accepts the " +
11
11
  "order id (e.g. '#W0000000' — note the leading '#'), or a spoken reference such as " +
@@ -33,6 +33,5 @@ export default retailTool({
33
33
  ...(order.cancel_reason ? { cancel_reason: order.cancel_reason } : {}),
34
34
  };
35
35
  },
36
- summary: (_args, result) =>
37
- isToolFailure(result) ? "order read failed" : `read ${result.order_id}`,
36
+ summary: (_args, result) => `read ${result.order_id}`,
38
37
  });
@@ -1,6 +1,6 @@
1
1
  import { isToolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { findProduct, retailTool, setFocus } from "../store.ts";
3
+ import { BEFORE_TRANSFER, findProduct, retailTool, setFocus } from "../store.ts";
4
4
 
5
5
  export default retailTool({
6
6
  name: "get_product_details",
@@ -10,7 +10,7 @@ export default retailTool({
10
10
  inputSchema: z.object({
11
11
  product_id: z.string().max(60).describe("The product id, e.g. '6086499569'"),
12
12
  }),
13
- requiresAuth: false,
13
+ when: BEFORE_TRANSFER,
14
14
  execute: (args, state) => {
15
15
  const product = findProduct(state, args.product_id);
16
16
  if (isToolFailure(product)) return product;
@@ -26,6 +26,5 @@ export default retailTool({
26
26
  })),
27
27
  };
28
28
  },
29
- summary: (_args, result) =>
30
- isToolFailure(result) ? "product read failed" : `read product ${result.name}`,
29
+ summary: (_args, result) => `read product ${result.name}`,
31
30
  });
@@ -4,6 +4,7 @@ import { requireOwnUser, retailTool } from "../store.ts";
4
4
 
5
5
  export default retailTool({
6
6
  name: "get_user_details",
7
+ when: "serving",
7
8
  description:
8
9
  "Get the authenticated customer's profile: name, email, default address, payment methods " +
9
10
  "(with gift-card balances) and their order ids with each order's status.",
@@ -32,6 +33,5 @@ export default retailTool({
32
33
  })),
33
34
  };
34
35
  },
35
- summary: (_args, result) =>
36
- isToolFailure(result) ? "profile read failed" : `read profile ${result.user_id}`,
36
+ summary: (_args, result) => `read profile ${result.user_id}`,
37
37
  });
@@ -1,6 +1,5 @@
1
- import { isToolFailure } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
- import { retailTool } from "../store.ts";
2
+ import { BEFORE_TRANSFER, retailTool } from "../store.ts";
4
3
 
5
4
  export default retailTool({
6
5
  name: "list_all_product_types",
@@ -10,13 +9,14 @@ export default retailTool({
10
9
  // Empty schema rather than omitting it: the wrapper has one code path, and
11
10
  // that is where the per-call UI-update invariant lives.
12
11
  inputSchema: z.object({}),
13
- requiresAuth: false,
12
+ // The catalogue is not customer data, so it needs no identified caller — but
13
+ // it is still off limits once the call belongs to a human.
14
+ when: BEFORE_TRANSFER,
14
15
  execute: (_args, state) => {
15
16
  const entries = Object.values(state.store.products)
16
17
  .map((product) => [product.name, product.product_id] as const)
17
18
  .sort(([a], [b]) => a.localeCompare(b));
18
19
  return { count: entries.length, products: Object.fromEntries(entries) };
19
20
  },
20
- summary: (_args, result) =>
21
- isToolFailure(result) ? "catalog read failed" : `listed ${result.count} product types`,
21
+ summary: (_args, result) => `listed ${result.count} product types`,
22
22
  });
@@ -6,6 +6,7 @@ import { retailTool, setFocus } from "../store.ts";
6
6
 
7
7
  export default retailTool({
8
8
  name: "modify_pending_order_address",
9
+ when: "serving",
9
10
  description:
10
11
  "Change the shipping address of a pending order. Read the new address back to the caller and " +
11
12
  "get an explicit yes before calling this. This does not change the customer's default address.",
@@ -34,6 +35,5 @@ export default retailTool({
34
35
  message: `Order ${order.order_id} now ships to ${formatAddress(order.address)}.`,
35
36
  };
36
37
  },
37
- summary: (_args, result) =>
38
- isToolFailure(result) ? "address change failed" : `re-addressed ${result.order_id}`,
38
+ summary: (_args, result) => `re-addressed ${result.order_id}`,
39
39
  });
@@ -6,6 +6,7 @@ import { applySwap, assertCanCoverDiff, planItemSwap } from "../swap.ts";
6
6
 
7
7
  export default retailTool({
8
8
  name: "modify_pending_order_items",
9
+ when: "serving",
9
10
  description:
10
11
  "Change items in a pending order to different options of the SAME products. This can be done " +
11
12
  "ONCE per order and is irreversible — afterwards the order can no longer be cancelled or " +
@@ -81,7 +82,5 @@ export default retailTool({
81
82
  };
82
83
  },
83
84
  summary: (_args, result) =>
84
- isToolFailure(result)
85
- ? "item change failed"
86
- : `modified ${result.order_id} (${result.price_difference >= 0 ? "+" : ""}${result.price_difference})`,
85
+ `modified ${result.order_id} (${result.price_difference >= 0 ? "+" : ""}${result.price_difference})`,
87
86
  });
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  export default retailTool({
14
14
  name: "modify_pending_order_payment",
15
+ when: "serving",
15
16
  description:
16
17
  "Change which payment method a pending order is charged to. The new method must be different " +
17
18
  "from the current one, and a gift card must hold enough to cover the whole order. The original " +
@@ -89,6 +90,5 @@ export default retailTool({
89
90
  message: `Order ${order.order_id} is now charged to ${args.payment_method_id}, and $${amount.toFixed(2)} is going back to ${original.payment_method_id}.`,
90
91
  };
91
92
  },
92
- summary: (_args, result) =>
93
- isToolFailure(result) ? "payment change failed" : `re-paid ${result.order_id}`,
93
+ summary: (_args, result) => `re-paid ${result.order_id}`,
94
94
  });
@@ -5,6 +5,7 @@ import { requireOwnUser, retailTool } from "../store.ts";
5
5
 
6
6
  export default retailTool({
7
7
  name: "modify_user_address",
8
+ when: "serving",
8
9
  description:
9
10
  "Change the customer's default address, used for future orders. Read the new address back and " +
10
11
  "get an explicit yes before calling this. This does not change the address on any existing " +
@@ -24,6 +25,5 @@ export default retailTool({
24
25
  message: `Default address updated to ${formatAddress(user.address)}. Existing orders keep their own shipping addresses.`,
25
26
  };
26
27
  },
27
- summary: (_args, result) =>
28
- isToolFailure(result) ? "profile address change failed" : `re-addressed ${result.user_id}`,
28
+ summary: (_args, result) => `re-addressed ${result.user_id}`,
29
29
  });
@@ -11,6 +11,7 @@ import {
11
11
 
12
12
  export default retailTool({
13
13
  name: "return_delivered_order_items",
14
+ when: "serving",
14
15
  description:
15
16
  "Request a return of items from a delivered order. Only a 'delivered' order can be returned, " +
16
17
  "and only once. The refund must go to the order's ORIGINAL payment method or to one of the " +
@@ -87,6 +88,5 @@ export default retailTool({
87
88
  message: `Return requested on ${order.order_id} for ${names}. The customer will get an email with return instructions, and the refund goes to ${args.payment_method_id} once the items arrive.`,
88
89
  };
89
90
  },
90
- summary: (_args, result) =>
91
- isToolFailure(result) ? "return failed" : `return requested on ${result.order_id}`,
91
+ summary: (_args, result) => `return requested on ${result.order_id}`,
92
92
  });
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { retailTool } from "../store.ts";
2
+ import { BEFORE_TRANSFER, retailTool } from "../store.ts";
3
3
 
4
4
  export default retailTool({
5
5
  name: "transfer_to_human_agents",
@@ -10,9 +10,13 @@ export default retailTool({
10
10
  inputSchema: z.object({
11
11
  summary: z.string().max(2000).describe("A short summary of the caller's issue for the human"),
12
12
  }),
13
- // No authentication: someone who cannot be identified is exactly who needs a
14
- // human, and blocking the escape hatch behind the gate would trap them.
15
- requiresAuth: false,
13
+ // Legal before the handoff from either side: someone who cannot be identified
14
+ // is exactly who needs a human, and blocking the escape hatch behind an auth
15
+ // gate would trap them. `TRANSFERRED` is the one transition into the terminal
16
+ // state, which is what makes "say nothing else after this" enforced rather
17
+ // than asked for — every tool, this one included, refuses afterwards.
18
+ when: BEFORE_TRANSFER,
19
+ send: { type: "TRANSFERRED" },
16
20
  summary: () => "transferred to a human agent",
17
21
  execute: (args) => ({
18
22
  transferred: true,