@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
@@ -1,8 +1,18 @@
1
- import { errorMessage, tool, toolFailure } from "@alexkroman1/aai";
1
+ import { errorMessage, ProcedureNotFinishedError, tool, toolFailure } from "@alexkroman1/aai";
2
2
  import { z } from "zod";
3
- import { runCorrectiveRag } from "../graph.ts";
3
+ import { runCorrectiveRag } from "../procedure.ts";
4
4
  import { recordQuestion, supportSlot } from "../shared.ts";
5
5
 
6
+ /**
7
+ * How long the whole corrective loop may run before it gives up.
8
+ *
9
+ * Under the runtime's own per-tool deadline (`TOOL_EXECUTION_TIMEOUT_MS`, 30s)
10
+ * with room for this tool's own bookkeeping, and written as a literal because
11
+ * that constant is on `@alexkroman1/aai/internal` — a subpath a template may
12
+ * not import. If the runtime's default ever moves, this is the number to move.
13
+ */
14
+ const LOOKUP_BUDGET_MS = 28_000;
15
+
6
16
  /**
7
17
  * The whole graph, as one tool.
8
18
  *
@@ -14,6 +24,33 @@ import { recordQuestion, supportSlot } from "../shared.ts";
14
24
  * The verdicts ride back with the answer rather than being swallowed. A
15
25
  * grounded-but-not-useful answer is still worth speaking, and the caller is the
16
26
  * one who should hear that it may not be the whole story.
27
+ *
28
+ * **The loop is given a BUDGET, because the runtime already has one and it is
29
+ * not this tool's.** The corrective path is up to eleven sequential model calls
30
+ * — two attempts of grade/generate/ground/use, plus a regeneration — and the
31
+ * tool executor abandons any tool at `TOOL_EXECUTION_TIMEOUT_MS` (30s), which
32
+ * an author cannot raise. Measured against a live gateway, the two-attempt path
33
+ * really does exceed it: the model was handed
34
+ * `{"error":"Tool \"answer_question\" timed out after 30000ms"}`, which tells
35
+ * it nothing it can act on and reads to a caller as the line going dead.
36
+ * `LOOKUP_BUDGET_MS` lands the overrun on the graph's OWN exit instead — the
37
+ * same "offer to look again, or log a ticket" the design already has for an
38
+ * interrupted run — so the slow path degrades into a sentence rather than into
39
+ * an internal error. It does not make the loop faster; it makes running out
40
+ * something the caller hears about.
41
+ *
42
+ * **Re-measured, and the loop is nowhere near the budget today.** Driven
43
+ * directly against the live gateway on the default model: the single-attempt
44
+ * path is 2.0-3.1s over four or five model calls, and the two-attempt
45
+ * CORRECTIVE path — nine calls, one query rewrite — is 2.8s, with each call
46
+ * 0.3-1.3s. A schema-constrained call is not the expensive kind either (572ms
47
+ * against 553ms for the same prompt without one), which is the guess anyone
48
+ * looking at this would make first. So the budget is a GUARD against a slow
49
+ * gateway rather than a bound the healthy path pushes against, and a question
50
+ * that trips a rewrite fits with room to spare. Keep it: the measurement above
51
+ * happened, and what changed is the service's latency, not the number of round
52
+ * trips. Do not go cutting round trips out of the graph on the strength of the
53
+ * old paragraph — measure first, the same way.
17
54
  */
18
55
  export default tool({
19
56
  description:
@@ -26,9 +63,34 @@ export default tool({
26
63
  async execute(args, ctx) {
27
64
  let trace: Awaited<ReturnType<typeof runCorrectiveRag>>;
28
65
  try {
29
- trace = await runCorrectiveRag(ctx.generate, args.question);
66
+ // `ctx.signal` is what stops the graph on a barge-in: this loop is five
67
+ // to nine model calls, and a caller who interrupts on the second should
68
+ // not be charged for the rest. The budget rides alongside it — see the
69
+ // module doc on `LOOKUP_BUDGET_MS` — and `AbortSignal.any` is what
70
+ // combines the two without any unlink bookkeeping.
71
+ trace = await runCorrectiveRag(
72
+ ctx.generate,
73
+ args.question,
74
+ AbortSignal.any([ctx.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)]),
75
+ );
30
76
  } catch (err: unknown) {
31
- // A broken model call is the tool's to report: the model can tell the
77
+ // An INTERRUPTED lookup is not a broken one, and the difference is worth
78
+ // a sentence: telling the model the knowledge base failed would have it
79
+ // apologize for an outage that did not happen. Which of the two signals
80
+ // fired decides WHICH sentence — a barge-in means the caller is already
81
+ // talking, and a spent budget means they are still waiting.
82
+ if (err instanceof ProcedureNotFinishedError) {
83
+ return toolFailure(
84
+ ctx.signal.aborted
85
+ ? "That lookup was cut short before it finished. Offer to look again, " +
86
+ "or to log a ticket with log_ticket."
87
+ : "That lookup ran out of time before it could be graded, so there is no " +
88
+ "answer to give. Say the check is taking too long, offer a narrower " +
89
+ "question, and offer to log a ticket with log_ticket. Do not answer " +
90
+ "from your own knowledge.",
91
+ );
92
+ }
93
+ // A broken model call IS the tool's to report: the model can tell the
32
94
  // caller the lookup failed, which is a better turn than silence.
33
95
  return toolFailure(`The knowledge base lookup failed: ${errorMessage(err)}`);
34
96
  }
@@ -0,0 +1,234 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ // An EVAL for a WORKFLOW APP: does the run actually do the work? Run it with
3
+ // `aai eval`.
4
+ //
5
+ // `agent.test.ts` asserts about the declaration, drives the four steps one at a
6
+ // time, and covers the WAV arithmetic as pure functions. This drives the WHOLE
7
+ // BODY — `transcribeFlow` from the top — and what it is here to check is the one
8
+ // thing no per-step spec can see: that a recording is really planned, really
9
+ // fanned out, and really stitched back into one transcript in the right order.
10
+ //
11
+ // `describeWorkflowEval` picks the providers for you and says which it picked:
12
+ //
13
+ // * with `ASSEMBLYAI_API_KEY` — a LIVE run. The `{ live: true }` case below
14
+ // downloads a real four-minute news clip, converts it with a real ffmpeg,
15
+ // and puts four real requests through the sync endpoint. That spends money
16
+ // and about a minute.
17
+ // * without one — a SCRIPTED run: the same body, the same plan, the same
18
+ // stitch, with the endpoint answered in memory.
19
+ //
20
+ // Two of the three cases are SCRIPTED IN BOTH MODES, deliberately. Their claims
21
+ // are about the PLAN and the SEAM — which window each request got, and what the
22
+ // merge does where two segments overlap — and those are facts about arithmetic
23
+ // that a live provider can neither confirm nor deny. The live case is the one
24
+ // that answers "is the transcript really of the recording".
25
+ //
26
+ // WHAT NO EVAL HERE COVERS: durability. Imported through vitest with no bundler
27
+ // in the path, a `"use workflow"` body is an ordinary async function — no
28
+ // journal, no replay, and no per-step retry, so the resume-after-segment-27
29
+ // property this template exists to demonstrate is NOT exercised here, and a
30
+ // rate-limited live run fails where a deployed one would have ridden it out.
31
+ // `aai-cli`'s `dev-workflow.scenario.test.ts` is the tier that really resumes a
32
+ // run.
33
+ import { encodeWav } from "@alexkroman1/aai/step";
34
+ import { installStubTranscribe, installStubUploads } from "@alexkroman1/aai/testing/vitest";
35
+ import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
36
+ import { expect } from "vitest";
37
+ import agentDef, { transcribe } from "./agent.ts";
38
+ import { TRANSCRIPT_STREAM } from "./workflows/stitch.ts";
39
+ import { SEGMENT_SECONDS } from "./workflows/wav.ts";
40
+
41
+ /** The id every case uploads under. */
42
+ const UPLOAD_ID = "upl_eval";
43
+
44
+ /** 16 kHz mono 16-bit — the format `normalize.ts` converts everything TO. */
45
+ const MONO_16K = { sampleRate: 16_000, channels: 1, bitsPerSample: 16 } as const;
46
+ /** Bytes one second of that format occupies. */
47
+ const BYTES_PER_SECOND = MONO_16K.sampleRate * MONO_16K.channels * (MONO_16K.bitsPerSample / 8);
48
+
49
+ /**
50
+ * Long enough to force a THREE-segment fan-out, which is the shape worth
51
+ * driving: one segment exercises no plan, two exercise one seam, three exercise
52
+ * a middle segment that is neither first nor last.
53
+ */
54
+ const SCRIPTED_SECONDS = 200;
55
+
56
+ /**
57
+ * A real, parseable, linear-PCM WAV of silence.
58
+ *
59
+ * Silence is fine here because the scripted cases never send it anywhere: what
60
+ * they assert is which BYTE RANGE each request was handed, and the bytes only
61
+ * have to be as long as the header says. It also takes the fast path through
62
+ * `normalizeRecording` — `parseWav` accepts it, so no ffmpeg is involved, which
63
+ * is what keeps these two cases runnable with no binary on `PATH`.
64
+ */
65
+ const SCRIPTED_WAV = encodeWav(new Uint8Array(SCRIPTED_SECONDS * BYTES_PER_SECOND), MONO_16K);
66
+
67
+ /** The public sample recording — four minutes of real speech, on a real CDN. */
68
+ const LIVE_RECORDING = "https://assembly.ai/wildfires.mp3";
69
+
70
+ /** Publish one in-memory upload store, writable because the converter needs one. */
71
+ function publish(bytes: Uint8Array, name: string, type: string) {
72
+ // `writable: true` is not optional for this template: `normalizeRecording`
73
+ // writes the CONVERTED file back as a new upload, and a read-only store would
74
+ // fail that step by name — which is the store telling the truth, and not what
75
+ // these cases are about.
76
+ return installStubUploads({ [UPLOAD_ID]: { bytes, name, type } }, { writable: true });
77
+ }
78
+
79
+ /**
80
+ * Does this request body carry a WAV header?
81
+ *
82
+ * Scanned as BYTES rather than decoded: the body is a multipart envelope around
83
+ * megabytes of audio, so `String(bytes)` is a comma-joined number list — which
84
+ * makes the assertion pass or fail for the wrong reason and prints five
85
+ * megabytes of it when it fails.
86
+ */
87
+ function carriesWavHeader(body: Uint8Array | string | undefined): boolean {
88
+ if (!(body instanceof Uint8Array)) return false;
89
+ const riff = [0x52, 0x49, 0x46, 0x46];
90
+ for (let at = 0; at <= body.length - riff.length; at++) {
91
+ if (riff.every((byte, offset) => body[at + offset] === byte)) return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /** The transcript chunks the run streamed, in the order they landed. */
97
+ function streamed(run: { emitted: readonly { namespace: string; chunk: unknown }[] }) {
98
+ return run.emitted
99
+ .filter((one) => one.namespace === TRANSCRIPT_STREAM)
100
+ .map((one) => one.chunk as { index: number; startMs: number; endMs: number; text: string });
101
+ }
102
+
103
+ describeWorkflowEval(agentDef, (test) => {
104
+ test("plans the fan-out from the header and gives each request its own window", async ({
105
+ app,
106
+ }) => {
107
+ // Scripted in both modes: the claim is which WINDOW each of the three
108
+ // requests was handed, and a live endpoint cannot answer that — it can only
109
+ // transcribe whatever it is sent. This is the case that catches an
110
+ // off-by-one in the plan, which otherwise produces audio the decoder
111
+ // transcribes into confident nonsense.
112
+ publish(SCRIPTED_WAV, "standup.wav", "audio/wav");
113
+ const provider = installStubTranscribe({
114
+ text: ["the first stretch", "the middle stretch", "the last stretch"],
115
+ });
116
+
117
+ const run = await app.run(transcribe, { recording: UPLOAD_ID });
118
+
119
+ expect(run.error).toBeUndefined();
120
+ expect(run.status).toBe("completed");
121
+ const output = run.output;
122
+ if (output === undefined) expect.fail("a completed run must carry an output");
123
+
124
+ // Three segments over 200 seconds at a 90-second stride, and the durations
125
+ // are the plan: each segment carries the 2-second OVERLAP that stops a cut
126
+ // landing mid-word, so they deliberately do not abut.
127
+ expect(output.segments).toBe(3);
128
+ expect(output.durationMs).toBe(SCRIPTED_SECONDS * 1000);
129
+ expect(streamed(run).map((chunk) => [chunk.index, chunk.startMs, chunk.endMs])).toEqual([
130
+ [0, 0, (SEGMENT_SECONDS + 2) * 1000],
131
+ [1, SEGMENT_SECONDS * 1000, (2 * SEGMENT_SECONDS + 2) * 1000],
132
+ [2, 2 * SEGMENT_SECONDS * 1000, SCRIPTED_SECONDS * 1000],
133
+ ]);
134
+
135
+ // One sync request per segment, each carrying a WHOLE WAV — the endpoint
136
+ // decodes every request independently, so a window with no header put back
137
+ // on it is bytes it refuses.
138
+ const sync = provider.calls.filter((call) => call.leg === "sync");
139
+ expect(sync).toHaveLength(3);
140
+ expect(sync.map((call) => carriesWavHeader(call.body))).toEqual([true, true, true]);
141
+ // The first two segments are a full stride plus the overlap; the last is the
142
+ // remainder, and is therefore much smaller. A plan that ran off the end of
143
+ // the file would show up here as a last segment the size of the others.
144
+ const sizes = sync.map((call) => call.body?.length ?? 0);
145
+ expect(sizes[0]).toBeGreaterThan((SEGMENT_SECONDS + 1) * BYTES_PER_SECOND);
146
+ expect(sizes[2]).toBeLessThan(sizes[0] ?? 0);
147
+
148
+ // Stitched in INDEX order, not completion order.
149
+ expect(output.transcript).toBe("the first stretch the middle stretch the last stretch");
150
+ // Reported before the fan-out, which is what makes a long run legible.
151
+ expect(run.reported).toContain("Split 3:20 of audio into 3 segments.");
152
+ });
153
+
154
+ test("drops the repeated words where two segments overlap", async ({ app }) => {
155
+ // Scripted in both modes for the same reason: the overlap exists so a cut
156
+ // never lands mid-word, and what makes that free is the merge dropping ONE
157
+ // copy of the repeated run. A live provider cannot be asked to produce a
158
+ // seam on demand, and this is where a regression would be invisible — a
159
+ // missed seam repeats a few words, a false one DELETES speech.
160
+ publish(SCRIPTED_WAV, "standup.wav", "audio/wav");
161
+ installStubTranscribe({
162
+ text: [
163
+ "the fire moved north through the valley overnight",
164
+ "through the valley overnight and reached the ridge by dawn",
165
+ "by dawn the crews had contained it",
166
+ ],
167
+ });
168
+
169
+ const run = await app.run(transcribe, { recording: UPLOAD_ID });
170
+
171
+ expect(run.error).toBeUndefined();
172
+ const transcript = run.output?.transcript ?? "";
173
+ expect(transcript).toBe(
174
+ "the fire moved north through the valley overnight and reached the ridge by dawn the crews had contained it",
175
+ );
176
+ // The repeated runs appear once each, which is the whole claim.
177
+ expect(transcript.match(/overnight/g)).toHaveLength(1);
178
+ expect(transcript.match(/by dawn/g)).toHaveLength(1);
179
+ });
180
+
181
+ test(
182
+ "really transcribes a real recording, end to end",
183
+ async ({ app }) => {
184
+ // LIVE ONLY, and it is the case that earns the template its name. Three
185
+ // separate things have to be real for it to pass: ffmpeg has to convert an
186
+ // MP3 into something the plan can cut, the plan has to cover the whole
187
+ // recording, and every segment's request has to come back with the words
188
+ // that are actually in it.
189
+ //
190
+ // It needs `ffmpeg` on `PATH` (or `AAI_FFMPEG_PATH`), which is what the
191
+ // template's own doc says a developer needs for anything that is not
192
+ // already a WAV. A deployed guest's image installs it.
193
+ const response = await fetch(LIVE_RECORDING);
194
+ expect(response.ok).toBe(true);
195
+ const mp3 = new Uint8Array(await response.arrayBuffer());
196
+ publish(mp3, "wildfires.mp3", "audio/mpeg");
197
+
198
+ const run = await app.run(transcribe, { recording: UPLOAD_ID });
199
+
200
+ expect(run.error).toBeUndefined();
201
+ expect(run.status).toBe("completed");
202
+ const output = run.output;
203
+ if (output === undefined) expect.fail("a completed run must carry an output");
204
+
205
+ // ffmpeg ran, and the run said so before spending minutes on it.
206
+ expect(run.reported.some((line) => line.startsWith("Converting wildfires.mp3"))).toBe(true);
207
+ // The FILENAME a reader sees is the one they uploaded, not the converted
208
+ // artifact's — `mergeTranscript` reads the ORIGINAL id for exactly this.
209
+ expect(output.source).toBe("wildfires.mp3");
210
+
211
+ // Four and a half minutes, so the plan really fanned out rather than
212
+ // sending one request.
213
+ expect(output.segments).toBeGreaterThanOrEqual(3);
214
+ expect(output.durationMs).toBeGreaterThan(250_000);
215
+
216
+ // Every segment came back with words in it, which is how "the whole
217
+ // recording was covered" is checked rather than assumed: a plan that ran
218
+ // past the end would leave a silent tail segment here.
219
+ const chunks = streamed(run);
220
+ expect(chunks).toHaveLength(output.segments);
221
+ for (const chunk of chunks) expect(chunk.text.length).toBeGreaterThan(0);
222
+
223
+ // And the transcript is of THIS recording — a news segment about smoke
224
+ // from Canadian wildfires reaching the US east coast.
225
+ expect(output.transcript).toMatch(/wildfire/i);
226
+ expect(output.transcript).toMatch(/canada/i);
227
+ expect(output.transcript).toMatch(/air quality/i);
228
+ // ~4.5 minutes of speech is several hundred words; a stitch that kept only
229
+ // the first segment would land far under this.
230
+ expect(output.words).toBeGreaterThan(400);
231
+ },
232
+ { live: true },
233
+ );
234
+ });