@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,366 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // An EVAL: does this desk actually behave? Run it with `aai eval`.
4
+ //
5
+ // `agent.test.ts` drives the five tools against a STUBBED `ctx.workflows`, the
6
+ // steps against a stubbed provider, and the body's two helpers with `sleep` and
7
+ // `createHook` mocked. What none of those tiers can say is whether the AGENT —
8
+ // a model, reading this system prompt, holding these five tools — hands a
9
+ // recording off at all, whether the run it starts is the same run a later turn
10
+ // finds, and whether a caller who says "forget it" is told the truth about what
11
+ // cancelling does. That is what this file is for, and it is only possible
12
+ // because `describeEval` opens a real workflow engine per case and hands its
13
+ // client to the session.
14
+ //
15
+ // Three boundaries, each of which a case here would be dishonest to imply:
16
+ //
17
+ // * **The engine is not durable.** No journal, no replay, no retry, and a
18
+ // `sleep()` is RECORDED rather than taken — so the poll loop below runs at
19
+ // full speed and the only way to observe a run in flight is to hold the
20
+ // provider's answer, which is what `hold` does.
21
+ // * **`createHook()` throws untransformed**, so the RETENTION GATE — this
22
+ // template's headline port of Temporal's `expense` — cannot be reached at
23
+ // all here, and neither can `keep_transcript`'s signal: a run that gets past
24
+ // `summarize` fails on the hook. Nothing below asserts the gate, its three
25
+ // outcomes, or its safe default; `agent.test.ts` drives
26
+ // `askWhetherToKeep` directly for those, and it is the only tier that can.
27
+ // * **The provider is scripted, in BOTH modes**, through `stepFetch`'s
28
+ // published slot — so the transcription, the recap's model call and the
29
+ // compensating DELETE are all deterministic and free, while the SESSION's
30
+ // model stays live and is what a live run measures.
31
+ //
32
+ // And what no eval here can see: anything below the audio boundary.
33
+ import { withDiscoveredTools } from "@alexkroman1/aai/testing";
34
+ import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
35
+ import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
36
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
37
+ import { expect } from "vitest";
38
+ import { z } from "zod";
39
+ import authoredAgent from "./agent.ts";
40
+ import { recap, SAMPLE_RECORDING } from "./shared.ts";
41
+
42
+ /**
43
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
44
+ *
45
+ * Load-bearing, and not applied by `agent()` — the BUILD enumerates `tools/`,
46
+ * so an eval driving the raw default export would measure a desk with none of
47
+ * its five tools. This template declares its prompt inline in `agent.ts`, so
48
+ * there is no `system-prompt.md` for `withSystemPrompt` to layer on.
49
+ *
50
+ * The glob is written here rather than reached for from a shared helper because
51
+ * this file SHIPS — see `agent.test.ts`.
52
+ */
53
+ const agentDef = withDiscoveredTools(
54
+ authoredAgent,
55
+ import.meta.glob("./tools/*.ts", { eager: true }),
56
+ );
57
+
58
+ /**
59
+ * The key every step reads with `requireStepEnv` — the one name `agent.ts`
60
+ * declares in `requiredEnv`.
61
+ *
62
+ * Passed as the agent env so the eval's workflow engine publishes it: the
63
+ * provider calls are answered by a fake, but each asks for the key BEFORE it
64
+ * makes its request, so a run with no key fails on the credential rather than
65
+ * reaching the script. The ENVIRONMENT and nothing else — a template may not
66
+ * read a developer's CLI config.
67
+ */
68
+ const EVAL_ENV = { ASSEMBLYAI_API_KEY: process.env.ASSEMBLYAI_API_KEY ?? "eval-scripted-key" };
69
+
70
+ /** The job id the scripted provider mints for every submission. */
71
+ const TRANSCRIPT_ID = "t_eval_1";
72
+
73
+ /** What the scripted transcript says, once it completes. */
74
+ const TRANSCRIPT_TEXT =
75
+ "Smoke from the Canadian wildfires reached the eastern seaboard overnight, " +
76
+ "and air quality indexes went into the unhealthy range.";
77
+
78
+ /** The recap the scripted model writes. `spoken` is the field the phone reads. */
79
+ const RECAP_JSON = JSON.stringify({
80
+ headline: "Wildfire smoke reaches the east coast",
81
+ points: ["Smoke crossed overnight", "Air quality is unhealthy", "Advisories are in force"],
82
+ spoken: "Wildfire smoke drifted east overnight and pushed air quality into the unhealthy range.",
83
+ });
84
+
85
+ /** How the scripted provider ends a job. */
86
+ type Ending = "completed" | "error";
87
+
88
+ /** A scripted provider, and the gate that holds its first status answer. */
89
+ type ScriptedProvider = {
90
+ /** Every step request, in order — what the run really asked the provider. */
91
+ readonly calls: readonly { url: string; method: string; body?: unknown }[];
92
+ /** Let a held first poll answer. Idempotent. */
93
+ readonly release: () => void;
94
+ };
95
+
96
+ /**
97
+ * Answer AssemblyAI's batch API and the LLM gateway in memory.
98
+ *
99
+ * Through `stepFetch`'s published slot rather than `vi.stubGlobal("fetch")`,
100
+ * which is the path a step really takes — and which leaves the SESSION's model
101
+ * live, so a live case still measures the agent. An unrecognised request THROWS
102
+ * rather than answering an empty 200: a step calling something nobody expected
103
+ * is a finding, where an empty body reads as a provider that said nothing.
104
+ *
105
+ * `hold` keeps the FIRST poll pending, and it is the only way to observe a run
106
+ * that is still going: a durable `sleep` is skipped here, so an unheld run
107
+ * burns its whole poll loop in milliseconds.
108
+ */
109
+ function stubProvider(options: { hold?: boolean; ending?: Ending } = {}): ScriptedProvider {
110
+ const gate = Promise.withResolvers<void>();
111
+ let polls = 0;
112
+ const stub = installStubStepFetch(async (request) => {
113
+ if (request.url.includes("/chat/completions")) {
114
+ return { body: { choices: [{ message: { content: RECAP_JSON } }] } };
115
+ }
116
+ if (request.method === "POST") return { body: { id: TRANSCRIPT_ID, status: "queued" } };
117
+ // The compensation. A real DELETE removes the transcript from the account,
118
+ // which is what makes "a failed run leaves nothing behind" a claim rather
119
+ // than a comment — so the assertion that matters is that this was CALLED.
120
+ if (request.method === "DELETE") return { body: {} };
121
+ if (request.method === "GET") {
122
+ polls += 1;
123
+ if (options.hold === true && polls === 1) await gate.promise;
124
+ return options.ending === "error"
125
+ ? { body: { status: "error", error: "that recording could not be decoded" } }
126
+ : { body: { status: "completed", text: TRANSCRIPT_TEXT, audio_duration: 254 } };
127
+ }
128
+ throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
129
+ });
130
+ return { calls: stub.calls, release: () => gate.resolve() };
131
+ }
132
+
133
+ /** `request_recap`'s two answers — it started one, or it found the live one. */
134
+ const RecapStart = z.union([
135
+ z.object({ started: z.literal(true), runId: z.string().min(1) }),
136
+ z.object({ started: z.literal(false), runId: z.string().min(1), note: z.string() }),
137
+ ]);
138
+
139
+ /** `cancel_recap`'s answer. */
140
+ const Cancelled = z.object({ cancelled: z.boolean(), note: z.string() });
141
+
142
+ /**
143
+ * Every `request_recap` answer in a turn, parsed.
144
+ *
145
+ * Parsed rather than regexed: a tool result reaches the event stream as a
146
+ * serialized string, and a shape that stopped matching should fail HERE naming
147
+ * the field instead of handing the next assertion `undefined`.
148
+ */
149
+ function recapStarts(calls: readonly EvalToolCall[]): z.infer<typeof RecapStart>[] {
150
+ return calls
151
+ .filter((one) => one.name === "request_recap" && one.result !== undefined)
152
+ .map((one) => RecapStart.parse(JSON.parse(String(one.result))));
153
+ }
154
+
155
+ /** The run id the first `request_recap` of this turn reported. */
156
+ function startedRunId(calls: readonly EvalToolCall[]): string {
157
+ const [first] = recapStarts(calls);
158
+ if (first === undefined) {
159
+ throw new Error(
160
+ `the desk called no request_recap: ${calls.map((one) => one.name).join(", ") || "(no tools)"}`,
161
+ );
162
+ }
163
+ return first.runId;
164
+ }
165
+
166
+ /** Every tool call in this turn that READS a run, whichever the model picked. */
167
+ function readbacks(calls: readonly EvalToolCall[]): readonly EvalToolCall[] {
168
+ return calls.filter((one) => one.name === "recap_status" || one.name === "recap_progress");
169
+ }
170
+
171
+ /** Requests of one method the run has made so far. */
172
+ function requests(provider: ScriptedProvider, method: string) {
173
+ return provider.calls.filter((one) => one.method === method);
174
+ }
175
+
176
+ /** `text` as a regex that matches only itself. */
177
+ function literal(text: string): string {
178
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
179
+ }
180
+
181
+ /** The one utterance every case that starts work opens with. */
182
+ const ASK = "Can you write up that recording for me?";
183
+
184
+ /** The scripted session turn that answers {@link ASK}. */
185
+ const START_TURN = [
186
+ { tool: "request_recap", args: {} },
187
+ "I've started writing that up — I'll let you know when it lands.",
188
+ ] as const;
189
+
190
+ /**
191
+ * Let the run finish before the case ends.
192
+ *
193
+ * Not tidiness: the scripted provider is unpublished when the test that
194
+ * installed it finishes, so a body still mid-flight would make its next request
195
+ * against whatever the next case publishes — or against the real provider, with
196
+ * a real key. Every run here ends FAILED, on `createHook()`; see the header.
197
+ */
198
+ async function drain(workflows: EvalWorkflows | undefined, provider: ScriptedProvider) {
199
+ provider.release();
200
+ for (const run of await (workflows?.runs() ?? [])) await workflows?.settle(run.runId, recap);
201
+ }
202
+
203
+ describeEval(
204
+ agentDef,
205
+ (test) => {
206
+ test(
207
+ "starts one run for the caller and will not pay for a second",
208
+ async ({ session, workflows }) => {
209
+ // Held, so the first run is unambiguously still live when the caller
210
+ // asks again — which is the state the live-run check exists for.
211
+ const provider = stubProvider({ hold: true });
212
+
213
+ const first = await session.say(ASK);
214
+ const runId = startedRunId(first.toolCalls);
215
+ const again = await session.say("Actually, start it again from scratch, please.");
216
+
217
+ // Temporal's workflow-id reuse policy, as this desk spells it: a caller
218
+ // who asks twice is told about the run they already have. WHETHER the
219
+ // model calls the tool a second time is its business — it may simply
220
+ // remember — so each call it did make has to have been refused with the
221
+ // run it found.
222
+ for (const answer of recapStarts(again.toolCalls)) {
223
+ expect(answer.started).toBe(false);
224
+ expect(answer.runId).toBe(runId);
225
+ }
226
+
227
+ // The half that is not vacuous either way, and the one the caller pays
228
+ // for: ONE run, and ONE submission to the provider.
229
+ const runs = await (workflows?.runs() ?? []);
230
+ expect(runs.map((one) => one.runId)).toEqual([runId]);
231
+ expect(runs[0]?.workflow).toBe("recap");
232
+ const posts = requests(provider, "POST");
233
+ expect(posts).toHaveLength(1);
234
+ // And it submitted the recording the DESK supplies, because a phone
235
+ // caller cannot read a URL aloud.
236
+ expect(JSON.parse(String(posts[0]?.body))).toMatchObject({
237
+ audio_url: SAMPLE_RECORDING,
238
+ speaker_labels: true,
239
+ });
240
+
241
+ await drain(workflows, provider);
242
+ },
243
+ {
244
+ stubReply: [
245
+ ...START_TURN,
246
+ { tool: "request_recap", args: {} },
247
+ "There's already one running for you — I'll tell you as soon as it lands.",
248
+ ],
249
+ },
250
+ );
251
+
252
+ test(
253
+ "reads the live run back rather than guessing at it",
254
+ async ({ session, workflows }) => {
255
+ const provider = stubProvider({ hold: true });
256
+
257
+ const started = await session.say(ASK);
258
+ const runId = startedRunId(started.toolCalls);
259
+ const turn = await session.say("How's that going?");
260
+
261
+ const read = readbacks(turn.toolCalls);
262
+ expect(read.length).toBeGreaterThan(0);
263
+ const answered = read.map((one) => one.result ?? "").join("\n");
264
+
265
+ // The load-bearing half: that readback happened while the run really
266
+ // was in flight, waiting on the provider — which is the only state
267
+ // these two tools exist for.
268
+ const runs = await (workflows?.runs() ?? []);
269
+ const live = runs.find((one) => one.runId === runId);
270
+ expect(live?.status).toBe("running");
271
+ expect(requests(provider, "GET")).toHaveLength(1);
272
+
273
+ // WHICH readback tool the model picks is its business — the prompt
274
+ // offers both — so the claim is about what it was TOLD, and each has its
275
+ // own shape: `recap_progress` hands back the run's own latest line and
276
+ // `recap_status` the snapshot's status. Compared against what the RUN
277
+ // really wrote rather than against a literal, because which line that is
278
+ // depends on how far the body got: with the durable `sleep` skipped here
279
+ // the `PATIENCE` race resolves at once, so the note the caller would
280
+ // hear two minutes in is already written.
281
+ const lastLine = String(live?.reported.at(-1));
282
+ expect(live?.reported.length).toBeGreaterThan(0);
283
+ expect(answered).toMatch(new RegExp(`${literal(lastLine)}|Still working on that one`));
284
+ expect(answered).not.toMatch(/Nothing started yet/);
285
+
286
+ await drain(workflows, provider);
287
+ },
288
+ {
289
+ stubReply: [
290
+ ...START_TURN,
291
+ { tool: "recap_progress", args: {} },
292
+ "It's with the transcription service now — nothing back yet.",
293
+ ],
294
+ },
295
+ );
296
+
297
+ test(
298
+ "cancelling says plainly what it did NOT roll back, and really does not",
299
+ async ({ session, workflows }) => {
300
+ const provider = stubProvider({ hold: true });
301
+
302
+ const started = await session.say(ASK);
303
+ const runId = startedRunId(started.toolCalls);
304
+ const turn = await session.say("Forget it — cancel that, please.");
305
+
306
+ const cancel = turn.toolCalls.find((one) => one.name === "cancel_recap");
307
+ const answer = Cancelled.parse(JSON.parse(String(cancel?.result)));
308
+ expect(answer.cancelled).toBe(true);
309
+ // The sentence is a documented promise of this template, not a
310
+ // decoration: cancellation is NOT cooperative here, so the transcript
311
+ // the run had already created stays on the account and the caller is
312
+ // told so rather than left to assume a rollback.
313
+ expect(answer.note).toMatch(/left behind/);
314
+ expect(answer.note).toMatch(/does not roll back/);
315
+
316
+ // And it is TRUE, which is the part only an eval with a real run can
317
+ // check: the run is cancelled, and no compensating DELETE went out.
318
+ const runs = await (workflows?.runs() ?? []);
319
+ expect(runs.find((one) => one.runId === runId)?.status).toBe("cancelled");
320
+ expect(requests(provider, "DELETE")).toEqual([]);
321
+
322
+ // Released after the assertions on purpose: the body runs on regardless
323
+ // (there is no queue here to stop delivering to, and Temporal's
324
+ // deliver-cancellation-into-the-workflow is the one thing this template
325
+ // says does not port), so anything it does afterwards is not what the
326
+ // caller was told about.
327
+ await drain(workflows, provider);
328
+ },
329
+ {
330
+ stubReply: [
331
+ ...START_TURN,
332
+ { tool: "cancel_recap", args: {} },
333
+ "Stopped it. The partial transcript stays on file — cancelling doesn't undo that.",
334
+ ],
335
+ },
336
+ );
337
+
338
+ test("a run that fails after creating a transcript deletes it again", async ({ workflows }) => {
339
+ // Started from the CASE rather than through a tool, because the subject
340
+ // is the saga and the failure has to be injected: the provider refuses
341
+ // the job, which is the branch that unwinds the compensation stack.
342
+ // `request_recap` is what the other three cases drive.
343
+ const provider = stubProvider({ ending: "error" });
344
+
345
+ const run = await workflows?.run(recap, {
346
+ url: SAMPLE_RECORDING,
347
+ requestedBy: "eval-session",
348
+ });
349
+
350
+ expect(run?.status).toBe("failed");
351
+ expect(run?.error).toMatch(/could not transcribe/);
352
+ // The unwind, off the run's own narration — one compensation, named.
353
+ const narration = run?.reported.join("\n") ?? "";
354
+ expect(narration).toMatch(/undoing 1 step/);
355
+ expect(narration).toMatch(`Discarding transcript ${TRANSCRIPT_ID}.`);
356
+ // And it really happened: the transcript this run created was deleted
357
+ // from the account, which is the promise "a failed recap leaves nothing
358
+ // behind" rests on. An undo registered BEFORE its step, or a `catch`
359
+ // that stopped compensating, fails here.
360
+ expect(requests(provider, "DELETE").map((one) => one.url)).toEqual([
361
+ `https://api.assemblyai.com/v2/transcript/${TRANSCRIPT_ID}`,
362
+ ]);
363
+ });
364
+ },
365
+ { env: EVAL_ENV },
366
+ );