@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
@@ -312,34 +312,3 @@ export function planSegments(format: WavFormat): Segment[] {
312
312
  }
313
313
  return segments;
314
314
  }
315
-
316
- /**
317
- * Put a canonical 44-byte WAV header in front of a slice of samples.
318
- *
319
- * A byte range of a WAV is raw PCM with no header, and the sync endpoint reads
320
- * the rate and channel count out of one. Sending `audio/pcm` with a `config`
321
- * part instead would work equally well; a header keeps the two parts of the
322
- * request from being able to disagree.
323
- */
324
- export function wavWithHeader(format: WavFormat, samples: Uint8Array): Uint8Array<ArrayBuffer> {
325
- const out = new Uint8Array(44 + samples.length);
326
- const view = new DataView(out.buffer);
327
- const write = (at: number, text: string) => {
328
- for (let i = 0; i < text.length; i++) view.setUint8(at + i, text.charCodeAt(i));
329
- };
330
-
331
- write(0, "RIFF");
332
- view.setUint32(4, 36 + samples.length, true);
333
- write(8, "WAVEfmt ");
334
- view.setUint32(16, 16, true); // `fmt ` payload length
335
- view.setUint16(20, 1, true); // linear PCM
336
- view.setUint16(22, format.channels, true);
337
- view.setUint32(24, format.sampleRate, true);
338
- view.setUint32(28, bytesPerSecond(format), true);
339
- view.setUint16(32, blockAlign(format), true);
340
- view.setUint16(34, format.bitsPerSample, true);
341
- write(36, "data");
342
- view.setUint32(40, samples.length, true);
343
- out.set(samples, 44);
344
- return out;
345
- }
@@ -0,0 +1,323 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // An EVAL: does the confirmation gate actually gate? Run it with `aai eval`.
4
+ //
5
+ // `agent.test.ts` drives each tool directly and asserts about the state it
6
+ // wrote. That is the right tier for "does `stageAction` refuse a second
7
+ // staging" — and it cannot answer the question this template exists to
8
+ // demonstrate, which is whether the gate still holds when a MODEL is the one
9
+ // picking tools. So these cases drive a real session and read the mechanism off
10
+ // the event stream: which tool ran, what the gated tool answered, and — the
11
+ // claim that matters — whether the caller's booking moved before they said yes.
12
+ //
13
+ // Two modes, per `describeEval`: with a key, a live model chooses the tools;
14
+ // without one, each case's `stubReply` scripts them. A scripted tool call really
15
+ // executes, so the flow gate really runs either way — what a stub run cannot
16
+ // tell you is whether the model would have chosen that tool.
17
+ //
18
+ // **`system-prompt.md` is applied HERE, not by `agent.ts`.** The build discovers
19
+ // the file, so an eval driving the raw default export would run this agent with
20
+ // the FRAMEWORK DEFAULT prompt — and the discipline that prompt imposes is the
21
+ // entire subject of this file. Measured against the default: the model answers
22
+ // product questions from its own knowledge and skips the tools the prompt exists
23
+ // to route it through, so a case run that way measures nothing it claims to.
24
+
25
+ import { withSystemPrompt } from "@alexkroman1/aai/manifest";
26
+ import { withDiscoveredTools } from "@alexkroman1/aai/testing";
27
+ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
28
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
29
+ import { expect } from "vitest";
30
+ import { z } from "zod";
31
+ import authoredAgent from "./agent.ts";
32
+ import systemPrompt from "./system-prompt.md?raw";
33
+
34
+ /**
35
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
36
+ *
37
+ * The glob is written HERE rather than reached for from a shared helper for the
38
+ * reason `agent.test.ts` gives — this file ships, so it may not import anything
39
+ * outside its own template. An eval that forgot it would run an agent with NO
40
+ * tools and read as a model that refuses to act.
41
+ */
42
+ const agentDef = withSystemPrompt(
43
+ withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
44
+ systemPrompt,
45
+ );
46
+
47
+ /**
48
+ * What the BROWSER is sent, as this eval reads it.
49
+ *
50
+ * Parsed rather than cast: `state.updated` carries `unknown`, so a projection
51
+ * that stopped matching FAILS naming the field, where the cast this replaced
52
+ * handed the assertions `undefined` and failed a line later on something else.
53
+ * It names only the fields asserted below, so `tripView` may grow without
54
+ * touching this.
55
+ */
56
+ const ProjectedTrip = z.object({
57
+ assistant: z.string(),
58
+ ticket: z.object({ flightId: z.string() }).nullable(),
59
+ bookings: z.array(z.unknown()),
60
+ pending: z.string().nullable(),
61
+ });
62
+
63
+ /**
64
+ * The `syncState` frames, in stream order, UP TO the first `confirm_action`.
65
+ *
66
+ * This is what makes "a staging tool mutates nothing" assertable against a live
67
+ * model: the caller's later yes is allowed to move the ticket, so a claim about
68
+ * the FINAL state is a claim about the conversation rather than about the gate.
69
+ * What the gate promises is that every frame before the confirmation shows the
70
+ * booking untouched — and `tripProjection` rides out on `state.updated` after
71
+ * every tool call, so the stream carries one per step.
72
+ */
73
+ function framesBeforeConfirm(session: EvalSession): z.infer<typeof ProjectedTrip>[] {
74
+ const views: z.infer<typeof ProjectedTrip>[] = [];
75
+ for (const event of session.events()) {
76
+ if (event.type === "tool.called" && event.toolName === "confirm_action") break;
77
+ if (event.type === "state.updated") views.push(ProjectedTrip.parse(event.state));
78
+ }
79
+ return views;
80
+ }
81
+
82
+ /**
83
+ * The latest frame — what the browser would be rendering now. `lastStateIn` is
84
+ * the SDK's reader for exactly this; the schema is why it is worth passing one.
85
+ */
86
+ const tripState = (session: EvalSession) => lastStateIn(session.events(), ProjectedTrip);
87
+
88
+ /**
89
+ * Drive a whole call, one caller line at a time, and hand back every turn.
90
+ *
91
+ * The cases below assert about the turn a MECHANISM fired in rather than about
92
+ * turn one, because how many turns a desk takes to get there is the model's
93
+ * business and it moved when the desk gate landed: the flight desk's brief says
94
+ * to search before quoting anything, so measured live this concierge now spends
95
+ * its first turn on `to_flight_assistant` and `search_flights` and reads the
96
+ * fare back before it stages. A case pinned to turn one is a flake with a
97
+ * misleading name — the same argument `retail`'s eval carries.
98
+ */
99
+ async function sayAll(session: EvalSession, lines: readonly string[]): Promise<EvalTurn[]> {
100
+ const turns: EvalTurn[] = [];
101
+ for (const line of lines) turns.push(await session.say(line));
102
+ return turns;
103
+ }
104
+
105
+ /** Every tool call of the call so far, flattened, in order. */
106
+ const callsIn = (turns: readonly EvalTurn[]) => turns.flatMap((turn) => turn.toolCalls);
107
+
108
+ /** The turn a named tool STAGED something in — the call that answered with the
109
+ * read-back rather than with a gate's refusal. */
110
+ const stagingTurn = (turns: readonly EvalTurn[], tool: string) =>
111
+ turns.find((turn) =>
112
+ turn.toolCalls.some(
113
+ (call) => call.name === tool && /awaitingConfirmation/.test(call.result ?? ""),
114
+ ),
115
+ );
116
+
117
+ describeEval(agentDef, (test) => {
118
+ test(
119
+ "a sensitive tool stages the change and moves nothing",
120
+ async ({ session }) => {
121
+ // Three lines, each of them the same REQUEST and none of them an answer to
122
+ // a read-back: what is asserted below is that the turn which staged did
123
+ // not also apply, so a line the model could read as consent ("correct",
124
+ // "that's right") would be measuring the caller instead of the desk.
125
+ const turns = await sayAll(session, [
126
+ "Move my ticket to flight LX52, the Wednesday one.",
127
+ "I want the Wednesday LX52 instead of the flight I'm on now.",
128
+ "Put me on LX52 on Wednesday, please.",
129
+ ]);
130
+
131
+ const staging = stagingTurn(turns, "update_ticket");
132
+ const attempts = callsIn(turns).filter((call) => call.name === "update_ticket");
133
+ // Named with the whole call, tools AND text: "expected undefined to be
134
+ // defined" says nothing about a desk that talked its way through three
135
+ // turns without staging, which is exactly the failure this case caught
136
+ // while the flight desk's brief had the read-back before the staging.
137
+ expect(
138
+ staging,
139
+ turns
140
+ .map(
141
+ (turn, i) =>
142
+ `turn ${i + 1}: [${turn.toolCalls.map((c) => c.name).join(", ")}] said: ${turn.text}`,
143
+ )
144
+ .join("\n"),
145
+ ).toBeDefined();
146
+ const staged = staging?.toolCalls.find((call) => call.name === "update_ticket");
147
+ // The tool answered with the read-back rather than with a receipt.
148
+ expect(staged?.result).toMatch(/awaitingConfirmation/);
149
+ // Any attempt that did NOT stage is the DESK GATE refusing:
150
+ // `update_ticket` belongs to the flight desk, so a model reaching for it
151
+ // before `to_flight_assistant` is told so and recovers inside the turn.
152
+ // That refusal is a legal step and it is asserted rather than tolerated —
153
+ // its own message is what pointed the model at the way in.
154
+ for (const attempt of attempts.filter((call) => call !== staged)) {
155
+ expect(attempt.result).toMatch(/to_flight_assistant|Not available yet/);
156
+ }
157
+ // The desk asks; it does not decide. A `confirm_action` AFTER the staging
158
+ // in the same turn is the agent confirming on its own initiative, which
159
+ // the system prompt forbids in as many words.
160
+ //
161
+ // After, not anywhere in the turn: measured live, a model that hears a
162
+ // second request as a yes reaches for `confirm_action` BEFORE it has
163
+ // staged anything, the gate refuses it (the case above is where that
164
+ // refusal is the subject), and it then stages properly. That is a wasted
165
+ // step rather than an unasked-for change, and folding the two together
166
+ // would fail this case for the behaviour the next one proves is safe.
167
+ // Narrowed first: `indexOf` takes a value, and `staged` is optional — the
168
+ // rewrite Biome offers for `findIndex` over an identity is UNSAFE for
169
+ // exactly that reason, and the assertion above is what makes an absent
170
+ // staging call a failure rather than a slice from 0.
171
+ const stagedAt = staged === undefined ? -1 : (staging?.toolCalls.indexOf(staged) ?? -1);
172
+ expect(staging?.toolCalls.slice(stagedAt + 1).map((call) => call.name) ?? []).not.toContain(
173
+ "confirm_action",
174
+ );
175
+
176
+ const views = framesBeforeConfirm(session);
177
+ const waiting = views.filter((view) => view.pending !== null);
178
+ expect(waiting.length, "no frame ever showed a staged change").toBeGreaterThan(0);
179
+ expect(waiting.at(-1)?.pending).toMatch(/LX52/);
180
+ // THE claim: through every frame up to the confirmation, the ticket is
181
+ // still the one the caller phoned in on. A staging tool that mutated
182
+ // would satisfy every assertion above this one.
183
+ for (const view of views) expect(view.ticket?.flightId).toBe("LX40");
184
+ },
185
+ {
186
+ stubReply: [
187
+ { tool: "to_flight_assistant", args: { request: "move my ticket to LX52" } },
188
+ { tool: "update_ticket", args: { flightId: "LX52" } },
189
+ "That would move you to LX52 on Wednesday, five ninety. Shall I go ahead?",
190
+ ],
191
+ },
192
+ );
193
+
194
+ test(
195
+ "confirm_action is refused while nothing is waiting",
196
+ async ({ session, mode }) => {
197
+ const turn = await session.say("Yes, I confirm — go ahead and do it.");
198
+
199
+ const attempts = turn.toolCalls.filter((call) => call.name === "confirm_action");
200
+ // In stub mode the script FORCES the call, so the gate is really
201
+ // exercised; a live model that declines to call it at all has honoured
202
+ // the same rule one level earlier, which is why the count is asserted
203
+ // only where it is determined.
204
+ if (mode === "stub") expect(attempts).toHaveLength(1);
205
+ for (const attempt of attempts) {
206
+ // The refusal names the position and quotes the state's instruction —
207
+ // that is what the model recovers from, and a gate that ran the body
208
+ // and then apologized would not carry it.
209
+ expect(attempt.result).toMatch(/Not available yet/);
210
+ expect(attempt.result).toMatch(/browsing/);
211
+ }
212
+ expect(tripState(session)?.pending ?? null).toBeNull();
213
+ expect(tripState(session)?.bookings ?? []).toHaveLength(0);
214
+ },
215
+ { stubReply: [{ tool: "confirm_action" }, "Nothing is waiting for your yes just now."] },
216
+ );
217
+
218
+ test(
219
+ "the caller's yes is the only thing that moves the ticket",
220
+ async ({ session }) => {
221
+ // Four lines, three of them a yes: which turn the desk stages in and which
222
+ // it applies in is its own business — the flight desk's brief has it
223
+ // search first — and saying yes repeatedly is what makes "once each"
224
+ // below a claim about the MECHANISM rather than about the model's pacing.
225
+ await sayAll(session, [
226
+ "Move my ticket to flight LX52, the Wednesday one.",
227
+ "Correct — LX52 on Wednesday. Please move my ticket to it.",
228
+ "Yes, that's right — go ahead and change it.",
229
+ "Yes. Confirm it, please.",
230
+ ]);
231
+
232
+ // The two gated tools' calls, in order.
233
+ const gated = session
234
+ .toolCalls()
235
+ .filter((call) => ["update_ticket", "confirm_action"].includes(call.name));
236
+ // "Did something" is read POSITIVELY — a staging answers
237
+ // `awaitingConfirmation`, an apply answers `applied` — so the complement
238
+ // is a claim rather than a definition.
239
+ const effective = gated.filter((call) =>
240
+ /awaitingConfirmation|"applied"/.test(call.result ?? ""),
241
+ );
242
+ // Staged first, applied second, once each. Reversed — or a confirm with no
243
+ // stage — is the regression this template's whole shape exists to prevent.
244
+ expect(effective.map((call) => call.name)).toEqual(["update_ticket", "confirm_action"]);
245
+ // Everything else has to be a GATE refusing, and nothing else: the desk
246
+ // gate turns away an `update_ticket` issued before
247
+ // `to_flight_assistant`, and the confirmation gate turns away a
248
+ // `confirm_action` issued before anything is staged (the case above is
249
+ // where that is the subject). Both are steps a live model really takes,
250
+ // neither moved the ticket, and neither is what this case is about.
251
+ for (const call of gated.filter((c) => !effective.includes(c))) {
252
+ expect(call.result, `${call.name} neither acted nor was refused`).toMatch(
253
+ /Not available yet|belongs to the/,
254
+ );
255
+ }
256
+
257
+ const applied = effective.at(-1);
258
+ expect(applied?.result).toMatch(/LX52/);
259
+
260
+ const view = tripState(session);
261
+ expect(view?.ticket?.flightId).toBe("LX52");
262
+ expect(view?.pending ?? null).toBeNull();
263
+ },
264
+ {
265
+ stubReply: [
266
+ // The delegation is in the script because the DESK GATE is real: the
267
+ // flight desk's tools refuse until `to_flight_assistant` has run, so a
268
+ // script that skipped it would exercise that refusal rather than this
269
+ // case's subject.
270
+ { tool: "to_flight_assistant", args: { request: "move my ticket to LX52" } },
271
+ { tool: "update_ticket", args: { flightId: "LX52" } },
272
+ "That's LX52 on Wednesday at five ninety. Confirm it?",
273
+ { tool: "confirm_action" },
274
+ "Done — you're on LX52 on Wednesday.",
275
+ ],
276
+ },
277
+ );
278
+
279
+ test(
280
+ "a hotel request goes to the hotel desk rather than being answered at the front",
281
+ async ({ session }) => {
282
+ const turn = await session.say(
283
+ "Leave the flight for now — I need a hotel in Boston for three nights, near the water.",
284
+ );
285
+
286
+ // The INDEX as well as the call, because what came before it is asserted
287
+ // below and `indexOf` on a possibly-undefined find is worse than both.
288
+ const handoffAt = turn.toolCalls.findIndex((call) => call.name === "to_hotel_assistant");
289
+ const handoff = turn.toolCalls[handoffAt];
290
+ expect(
291
+ handoff,
292
+ `tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
293
+ ).toBeDefined();
294
+ // The brief IS the tool result, which is the whole port of their
295
+ // per-assistant prompt onto a session whose prompt is fixed at connect.
296
+ expect(handoff?.result).toMatch(/hotel desk/);
297
+ expect(handoff?.args.request).toBeTruthy();
298
+ // And the stack moved, which is what the caller's sidebar renders and what
299
+ // `complete_or_escalate` will pop.
300
+ expect(tripState(session)?.assistant).toBe("hotel");
301
+ // Anything the model reached for BEFORE delegating was refused, and so
302
+ // told nothing about Boston hotels: the desk gate is what makes the
303
+ // handoff above unavoidable rather than requested. This is the assertion
304
+ // that would have caught the old behaviour even if the model had happened
305
+ // to delegate afterwards.
306
+ const before = turn.toolCalls.slice(0, handoffAt);
307
+ for (const call of before.filter((c) => c.name.startsWith("search_"))) {
308
+ expect(call.result).toMatch(/belongs to the/);
309
+ }
310
+ },
311
+ // Live only: which desk the model hands the call to is exactly the judgement
312
+ // a script would be making on its behalf.
313
+ //
314
+ // This case is what turned the narrowing into a MECHANISM. It was written
315
+ // when the delegation was asked for in the prompt and nothing enforced it —
316
+ // one session has one tool list — and it measured the asking losing 0 of 5:
317
+ // the model called `search_hotels` from the concierge desk every run. Every
318
+ // desk tool now checks the stack (`requireDesk` in `shared.ts`), so what
319
+ // this measures is that the refusal really does route the model through the
320
+ // desk, inside one turn.
321
+ { live: true },
322
+ );
323
+ });