@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
@@ -19,7 +19,7 @@
19
19
  * The first three are ordinary step work. The fourth was impossible until two
20
20
  * things existed, and they are what this template is the reference use of:
21
21
  *
22
- * - **`stepSpeak`** (`@alexkroman1/aai/utils`) synthesizes from inside a step.
22
+ * - **`stepSpeak`** (`@alexkroman1/aai/step`) synthesizes from inside a step.
23
23
  * The session TTS surface cannot be used here at all: a `TtsSession` is an
24
24
  * event stream wired into a live pipeline's playback, with a turn tracker and
25
25
  * barge-in behind it, and a step has no turn to be part of and has to return
@@ -76,8 +76,9 @@
76
76
 
77
77
  import { workflow, workflowApp } from "@alexkroman1/aai";
78
78
  import { ASSEMBLYAI_TTS_DEFAULT_VOICE, ASSEMBLYAI_TTS_VOICES } from "@alexkroman1/aai/tts";
79
+ import type { WorkflowDef } from "@alexkroman1/aai/workflow-api";
79
80
  import { z } from "zod";
80
- import { spokenSummaryFlow } from "./workflows/summarize.ts";
81
+ import { type SpokenSummary, spokenSummaryFlow } from "./workflows/summarize.ts";
81
82
 
82
83
  /**
83
84
  * The voices the form offers.
@@ -102,28 +103,40 @@ const VOICES = Object.entries(ASSEMBLYAI_TTS_VOICES)
102
103
  */
103
104
  const [FIRST_VOICE = ASSEMBLYAI_TTS_DEFAULT_VOICE, ...OTHER_VOICES] = VOICES;
104
105
 
106
+ /**
107
+ * The run input, as its own const.
108
+ *
109
+ * Named rather than inline because {@link spokenSummary} carries an explicit
110
+ * type, and that annotation is what lets `workflows/summarize.ts` name
111
+ * `WorkflowInputOf<typeof spokenSummary>` for its body's parameter: the body's
112
+ * own signature would otherwise be part of what infers this declaration's type,
113
+ * and TypeScript refuses the cycle (`TS7022`).
114
+ */
115
+ const spokenSummaryInput = z.object({
116
+ // A plain string, because an upload id is what the run really receives.
117
+ // What makes it a file picker rather than a text box is the `uploads` line
118
+ // below.
119
+ recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
120
+ // An enum, so the form renders a SELECT rather than a text box — which is
121
+ // the whole reason the list is derived above rather than left free-form.
122
+ // Optional, so the SDK's own default voice applies when nobody chooses.
123
+ voice: z
124
+ .enum([FIRST_VOICE, ...OTHER_VOICES])
125
+ .optional()
126
+ .describe("Voice to read the summary in"),
127
+ });
128
+
105
129
  /**
106
130
  * The declaration: schema, description, and the directive body.
107
131
  *
108
132
  * Exported so `WorkflowOutputOf<typeof spokenSummary>` names the output type in
109
133
  * one place — including from `client.tsx`, where `import type` is erased and so
110
- * bundles nothing server-side.
134
+ * bundles nothing server-side — and so `workflows/summarize.ts` can name
135
+ * `WorkflowInputOf<typeof spokenSummary>` for the body's parameter.
111
136
  */
112
- export const spokenSummary = workflow({
137
+ export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary> = workflow({
113
138
  description: "Transcribe a recording, summarize it, and read the summary back as audio",
114
- input: z.object({
115
- // A plain string, because an upload id is what the run really receives.
116
- // What makes it a file picker rather than a text box is the `uploads` line
117
- // below.
118
- recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
119
- // An enum, so the form renders a SELECT rather than a text box — which is
120
- // the whole reason the list is derived above rather than left free-form.
121
- // Optional, so the SDK's own default voice applies when nobody chooses.
122
- voice: z
123
- .enum([FIRST_VOICE, ...OTHER_VOICES])
124
- .optional()
125
- .describe("Voice to read the summary in"),
126
- }),
139
+ input: spokenSummaryInput,
127
140
  // The one line that makes the form take a file: `<WorkflowFields>` renders a
128
141
  // picker for this property, `useWorkflowSubmit` stores the chosen file, and
129
142
  // the step that transcribes it reads it back with `readUpload`.
@@ -36,18 +36,19 @@ import "@alexkroman1/aai-ui/styles.css";
36
36
  // ERASED at build time, so naming the agent's own type costs the browser bundle
37
37
  // nothing — and it is what stops this file restating a shape
38
38
  // `workflows/summarize.ts` already declares.
39
- import type { WorkflowOutputOf } from "@alexkroman1/aai";
39
+ import { formatDuration } from "@alexkroman1/aai/utils";
40
+ import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
40
41
  import {
41
42
  createWorkflowApi,
42
43
  Form,
43
44
  page,
44
45
  SubmitButton,
45
46
  UploadProgressBar,
47
+ useDownloadUrl,
46
48
  useWorkflowSubmit,
47
49
  WorkflowFields,
48
50
  WorkflowProgress,
49
51
  } from "@alexkroman1/aai-ui";
50
- import { useEffect, useState } from "react";
51
52
  import type { spokenSummary } from "./agent.ts";
52
53
 
53
54
  /** What a completed run reports, derived from the workflow rather than restated. */
@@ -84,58 +85,16 @@ function captionsUrl(text: string, durationMs: number): string {
84
85
  return `data:text/vtt;charset=utf-8,${encodeURIComponent(vtt)}`;
85
86
  }
86
87
 
87
- /** Seconds a person can read, from the milliseconds a run reports. */
88
- function duration(ms: number): string {
89
- const total = Math.round(ms / 1000);
90
- return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
91
- }
92
-
93
- /**
94
- * The finished run's audio, as something the browser will play.
95
- *
96
- * A hook rather than four lines in the component because the CLEANUP is the
97
- * part worth keeping in one place: an object URL pins its blob for the life of
98
- * the document, so it is revoked when the id changes and when the page goes
99
- * away. The `cancelled` flag covers the other half — a second run settling
100
- * while the first download is still in flight would otherwise set state from
101
- * the stale one.
102
- */
103
- function useAudioUrl(uploadId: string | undefined): { url?: string; error?: string } {
104
- const [state, setState] = useState<{ url?: string; error?: string }>({});
105
-
106
- useEffect(() => {
107
- if (uploadId === undefined) {
108
- setState({});
109
- return;
110
- }
111
- let cancelled = false;
112
- let objectUrl: string | undefined;
113
- api
114
- .download(uploadId)
115
- .then((blob) => {
116
- if (cancelled) return;
117
- objectUrl = URL.createObjectURL(blob);
118
- setState({ url: objectUrl });
119
- })
120
- .catch((err: unknown) => {
121
- if (!cancelled) setState({ error: err instanceof Error ? err.message : String(err) });
122
- });
123
- return () => {
124
- cancelled = true;
125
- if (objectUrl !== undefined) URL.revokeObjectURL(objectUrl);
126
- };
127
- }, [uploadId]);
128
-
129
- return state;
130
- }
131
-
132
88
  export function App() {
133
89
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
134
90
  // `run.output` instead of `unknown`.
135
91
  const { submit, run, pending, upload, pauseUpload, resumeUpload, error } =
136
92
  useWorkflowSubmit<Summary>(WORKFLOW, { api });
137
93
  const output = run?.status === "completed" ? run.output : undefined;
138
- const audio = useAudioUrl(output?.audio);
94
+ // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
95
+ // bytes have to be FETCHED and handed to the element as an object URL — and the
96
+ // object URL has to be revoked, which is the half a page written by hand forgets.
97
+ const audio = useDownloadUrl(output?.audio, { api });
139
98
 
140
99
  return (
141
100
  <main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
@@ -169,7 +128,7 @@ export function App() {
169
128
  <div className="flex flex-col gap-1">
170
129
  <h2 className="text-xl">{output.headline}</h2>
171
130
  <p className="text-sm opacity-70">
172
- {output.source} · {duration(output.durationMs)} · {output.words} words
131
+ {output.source} · {formatDuration(output.durationMs)} · {output.words} words
173
132
  </p>
174
133
  </div>
175
134
 
@@ -181,8 +140,9 @@ export function App() {
181
140
 
182
141
  <section className="flex flex-col gap-2">
183
142
  <h3 className="text-sm font-medium opacity-70">
184
- Read aloud · {duration(output.audioDurationMs)}
143
+ Read aloud · {formatDuration(output.audioDurationMs)}
185
144
  </h3>
145
+ {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
186
146
  {audio.error !== undefined && (
187
147
  <p className="text-red-600">Could not load the audio: {audio.error}</p>
188
148
  )}
@@ -24,7 +24,7 @@
24
24
  * run. The bytes go to the store, the output carries the id, and the page
25
25
  * turns it back into something to play with `api.download(id)`.
26
26
  *
27
- * Both are on `@alexkroman1/aai/utils`, imported from THERE rather than the
27
+ * Both are on `@alexkroman1/aai/step`, imported from THERE rather than the
28
28
  * root: a `workflows/*.ts` module is bundled separately by the WDK builder, so
29
29
  * the root barrel's module graph would ride into the step bundle.
30
30
  *
@@ -49,19 +49,17 @@
49
49
  * expensive one.
50
50
  */
51
51
 
52
- import { throwStepError } from "@alexkroman1/aai/step-errors";
53
- import {
54
- omitUndefined,
55
- report,
56
- stepGenerateJson,
57
- stepSpeak,
58
- TRANSCRIBE_API,
59
- writeUpload,
60
- } from "@alexkroman1/aai/utils";
52
+ import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
53
+ import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
54
+ import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
55
+ // ERASED at build time, so the body can name the schema's own output type without
56
+ // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
57
+ // for `WorkflowOutputOf`.
58
+ import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
61
59
  import { sleep } from "workflow";
62
60
  import { z } from "zod";
61
+ import type { spokenSummary } from "../agent.ts";
63
62
  import {
64
- countWords,
65
63
  createJob,
66
64
  MAX_POLLS,
67
65
  POLL_INTERVAL,
@@ -128,13 +126,9 @@ export type SpokenSummary = {
128
126
  };
129
127
 
130
128
  /** Transcribe a recording, summarize it, and read the summary back. */
131
- export async function spokenSummaryFlow(input: {
132
- recording: string;
133
- // `| undefined` explicitly, not merely optional: `exactOptionalPropertyTypes`
134
- // is on repo-wide, and what a zod `.optional()` infers is a property that may
135
- // be PRESENT and undefined.
136
- voice?: string | undefined;
137
- }): Promise<SpokenSummary> {
129
+ export async function spokenSummaryFlow(
130
+ input: WorkflowInputOf<typeof spokenSummary>,
131
+ ): Promise<SpokenSummary> {
138
132
  "use workflow";
139
133
 
140
134
  const transcript = await transcribe(input.recording);
@@ -188,7 +182,7 @@ export async function summarize(
188
182
  "use step";
189
183
 
190
184
  await report("Summarizing the transcript.");
191
- const reply = await stepGenerateJson(
185
+ const reply = await stepGenerateJsonClassified(
192
186
  "Summarize this transcript of a recording.\n\n" +
193
187
  "Answer with JSON only, in this shape:\n" +
194
188
  `{"headline": "...", "points": ["..."], "spoken": "..."}\n\n` +
@@ -204,9 +198,10 @@ export async function summarize(
204
198
  system: "You summarize recordings. You answer with JSON and nothing else.",
205
199
  schema: SummaryReply,
206
200
  },
207
- // Classified off the gateway's own status: a 429 is worth another attempt
208
- // and a 400 is not, and `throwStepError` is what tells the DevKit which.
209
- ).catch(throwStepError);
201
+ // The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which
202
+ // reads the gateway's own status: a 429 is worth another attempt and a 400
203
+ // is not, and that is what tells the DevKit which.
204
+ );
210
205
 
211
206
  return { headline: reply.headline, points: reply.points.slice(0, POINTS), spoken: reply.spoken };
212
207
  }
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * **Every one of them is four lines, because the SDK owns the endpoint.**
14
14
  * `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
15
- * `@alexkroman1/aai/utils` carry the URL, the raw-key auth, the windowed
15
+ * `@alexkroman1/aai/step` reached here through their `*Classified` callers on
16
+ * `@alexkroman1/aai/step-errors` — carry the URL, the raw-key auth, the windowed
16
17
  * streaming upload, the PLURAL `speech_models` field and the failure
17
18
  * classification — all of which this file used to spell out, and all of which
18
19
  * `transcription-workflow` used to spell out again, differently worded and
@@ -37,14 +38,13 @@
37
38
  * the one leg that should be as boring as possible.
38
39
  */
39
40
 
40
- import { throwStepError } from "@alexkroman1/aai/step-errors";
41
+ import { report, uploadInfo } from "@alexkroman1/aai/step";
41
42
  import {
42
- report,
43
- stepTranscribePoll,
44
- stepTranscribeSubmit,
45
- stepTranscribeUpload,
46
- uploadInfo,
47
- } from "@alexkroman1/aai/utils";
43
+ stepTranscribePollClassified,
44
+ stepTranscribeSubmitClassified,
45
+ stepTranscribeUploadClassified,
46
+ } from "@alexkroman1/aai/step-errors";
47
+ import { countWords, formatBytes } from "@alexkroman1/aai/utils";
48
48
 
49
49
  /** How long between polls of a submitted job. */
50
50
  export const POLL_INTERVAL = "10s";
@@ -79,16 +79,20 @@ export type Transcript = {
79
79
  * expires before the next step runs; that costs one fresh upload, once, instead
80
80
  * of five.
81
81
  *
82
- * `.catch(throwStepError)` is what turns the SDK's `TranscribeError` into the
83
- * DevKit's verdict a missing key and a 400 stop, a 429 waits as long as the
84
- * service asked. Every step here ends the same way for the same reason.
82
+ * The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
83
+ * `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns
84
+ * the SDK's `TranscribeError` into the DevKit's verdict a missing key and a
85
+ * 400 stop, a 429 waits as long as the service asked. Every step here ends the
86
+ * same way for the same reason.
85
87
  */
86
88
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
87
89
  "use step";
88
90
 
89
91
  const stored = await uploadInfo(uploadId);
90
- await report(`Uploading ${stored.name || uploadId} (${mb(stored.size)}) for transcription.`);
91
- return await stepTranscribeUpload(uploadId).catch(throwStepError);
92
+ await report(
93
+ `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
94
+ );
95
+ return await stepTranscribeUploadClassified(uploadId);
92
96
  }
93
97
 
94
98
  /** Retries beyond the default 3: an upload is the one call here worth another attempt. */
@@ -98,7 +102,7 @@ uploadToProvider.maxRetries = 5;
98
102
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
99
103
  "use step";
100
104
 
101
- const job = await stepTranscribeSubmit(audioUrl).catch(throwStepError);
105
+ const job = await stepTranscribeSubmitClassified(audioUrl);
102
106
  await report(`Transcribing — job ${job.id}.`);
103
107
  return job;
104
108
  }
@@ -118,7 +122,7 @@ export async function pollTranscript(
118
122
  ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
119
123
  "use step";
120
124
 
121
- const progress = await stepTranscribePoll(id).catch(throwStepError);
125
+ const progress = await stepTranscribePollClassified(id);
122
126
  if (!progress.done) return { done: false };
123
127
 
124
128
  const stored = await uploadInfo(uploadId);
@@ -132,14 +136,3 @@ export async function pollTranscript(
132
136
  },
133
137
  };
134
138
  }
135
-
136
- /** Words in a transcript, for the counts a page shows. */
137
- export function countWords(text: string): number {
138
- const trimmed = text.trim();
139
- return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
140
- }
141
-
142
- /** A size a person can read, because the number that matters is the scale. */
143
- function mb(bytes: number): string {
144
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
145
- }
@@ -0,0 +1,273 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // An EVAL: does the line really refuse to say what it cannot ground? Run it
4
+ // with `aai eval`.
5
+ //
6
+ // `agent.test.ts` drives the retriever and each CRAG node directly. What it
7
+ // cannot see is the claim this template exists to make — that a MODEL holding
8
+ // this tool list cannot state a fee it did not just retrieve. So these cases
9
+ // drive a real session and read the verdicts off the tool results: which
10
+ // documents the grader passed, whether the answer was grounded, and whether an
11
+ // answer it could not ground was WITHHELD.
12
+ //
13
+ // **One thing a SCRIPTED run cannot reach: `ctx.generate` with a SCHEMA.** The
14
+ // stub model registers as an LLM provider, so `ctx.generate` resolves it, and it
15
+ // answers a plain text generation from the script. Every node in this template's
16
+ // loop passes a `schema` though (`binaryScore`), and that overload comes back
17
+ // `{"error":"The knowledge base lookup failed: No output generated."}` — so the
18
+ // whole corrective loop is unreachable scripted. That is the template's central
19
+ // tool, so the two cases that run without a key are written as invariants both
20
+ // modes satisfy (a failed lookup is REPORTED, never answered around), and the
21
+ // two that read a real verdict are `{ live: true }`.
22
+ //
23
+ // **`system-prompt.md` is applied HERE, not by `agent.ts`.** The build discovers
24
+ // the file, so an eval driving the raw default export would run this agent with
25
+ // the FRAMEWORK DEFAULT prompt — and the discipline that prompt imposes is the
26
+ // entire subject of this file. Measured against the default: the model answers
27
+ // product questions from its own knowledge and skips the tools the prompt exists
28
+ // to route it through, so a case run that way measures nothing it claims to.
29
+
30
+ import { withSystemPrompt } from "@alexkroman1/aai/manifest";
31
+ import { withDiscoveredTools } from "@alexkroman1/aai/testing";
32
+ import { type EvalSession, toolResultIn } from "@alexkroman1/aai-runtime/eval";
33
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
34
+ import { expect } from "vitest";
35
+ import { z } from "zod";
36
+ import authoredAgent from "./agent.ts";
37
+ import systemPrompt from "./system-prompt.md?raw";
38
+
39
+ /** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
40
+ const agentDef = withSystemPrompt(
41
+ withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
42
+ systemPrompt,
43
+ );
44
+
45
+ /**
46
+ * What the BROWSER is sent, as this eval reads it.
47
+ *
48
+ * Parsed rather than cast: `state.updated` carries `unknown`, so a projection
49
+ * that stopped matching FAILS naming the field, where the cast this replaced
50
+ * handed the assertions `undefined` and failed a line later on something else.
51
+ * It names only the fields asserted below, so `supportView` may grow without
52
+ * touching this.
53
+ */
54
+ const ProjectedSupport = z.object({
55
+ ticket: z.string().nullable(),
56
+ trace: z
57
+ .object({
58
+ exhausted: z.boolean(),
59
+ docs: z.array(z.object({ relevant: z.boolean() })),
60
+ steps: z.array(z.object({ node: z.string() })),
61
+ })
62
+ .nullable(),
63
+ });
64
+
65
+ /**
66
+ * What `answer_question` answered, validated.
67
+ *
68
+ * All-optional, and that is the shape of the CONTRACT rather than laxness: the
69
+ * tool has three legal outcomes — a graded answer, a withheld one carrying
70
+ * guidance, and a reported failure — so a required field would fail two of the
71
+ * three. What the schema still buys is that a field the case reads is the type
72
+ * the case thinks it is.
73
+ */
74
+ const Lookup = z.object({
75
+ answer: z.string().nullable().optional(),
76
+ sources: z.array(z.string()).optional(),
77
+ grounded: z.boolean().optional(),
78
+ answersTheQuestion: z.boolean().optional(),
79
+ rewrites: z.number().optional(),
80
+ guidance: z.string().optional(),
81
+ error: z.string().optional(),
82
+ });
83
+
84
+ /** Every `syncState` frame, in stream order. */
85
+ function frames(session: EvalSession) {
86
+ return session
87
+ .events()
88
+ .flatMap((event) =>
89
+ event.type === "state.updated" ? [ProjectedSupport.parse(event.state)] : [],
90
+ );
91
+ }
92
+
93
+ /**
94
+ * What `answer_question` answered, in call order.
95
+ *
96
+ * `toolResultIn` over a ONE-CALL list per call: the name is that call's own, so
97
+ * the reader's "no such call" and "two calls" throws are unreachable and what is
98
+ * left is the parse, the schema, and its "never completed" failure — which is
99
+ * the half the hand-rolled `JSON.parse(call.result ?? "null")` this replaced got
100
+ * wrong, reading `null` for a tool that never came back.
101
+ */
102
+ function lookups(session: EvalSession) {
103
+ return session
104
+ .toolCalls()
105
+ .filter((call) => call.name === "answer_question")
106
+ .map((call) => toolResultIn([call], call.name, Lookup));
107
+ }
108
+
109
+ describeEval(agentDef, (test) => {
110
+ test(
111
+ "nothing about the product is said without going through the knowledge base",
112
+ async ({ session }) => {
113
+ const turn = await session.say("How much notice do I have to give if I want to cancel?");
114
+
115
+ // The prompt's one rule: `answer_question` is the only route to anything
116
+ // about Meridian Fibre. A model that answered a notice period from its own
117
+ // knowledge would sound identical down a phone.
118
+ const asked = turn.toolCalls.filter((call) => call.name === "answer_question");
119
+ // The reply rides in the message, because the failure that matters here is
120
+ // a turn that SPOKE without looking anything up — "let me check that for
121
+ // you" and then nothing, or worse, a notice period from memory.
122
+ expect(
123
+ asked.length,
124
+ `tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
125
+ ).toBe(1);
126
+
127
+ for (const payload of lookups(session)) {
128
+ // Three legal outcomes, and the invariant that spans them: an answer the
129
+ // caller is given is an answer the hallucination grader passed. `grounded:
130
+ // false` reaching the model is the one thing the whole apparatus exists
131
+ // to prevent — and a broken lookup is REPORTED rather than worked around.
132
+ if (payload.error !== undefined) continue;
133
+ if (payload.answer === null) continue;
134
+ expect(payload.grounded, `ungrounded answer handed to the model: ${payload.answer}`).toBe(
135
+ true,
136
+ );
137
+ }
138
+ },
139
+ {
140
+ stubReply: [{ tool: "answer_question", args: { question: "notice to cancel" } }, "One sec."],
141
+ },
142
+ );
143
+
144
+ test(
145
+ "a logged ticket's callback number never leaves the server",
146
+ async ({ session }) => {
147
+ // Two turns, because that is the shape the prompt asks for: the lookup comes
148
+ // back empty, the line offers a ticket, and only then is there a number to
149
+ // take. Asked for in one breath, a live model reasonably answers the offer
150
+ // instead of acting on it.
151
+ await session.say("Can I add a pay-monthly mobile SIM to my broadband account?");
152
+ await session.say("Yes please, log that one — my callback number is 07700 900123.");
153
+
154
+ const logged = session.toolCalls().find((call) => call.name === "log_ticket");
155
+ expect(
156
+ logged,
157
+ `tools called: ${session
158
+ .toolCalls()
159
+ .map((c) => c.name)
160
+ .join(", ")}`,
161
+ ).toBeDefined();
162
+ expect(logged?.result).toMatch(/TCK\d{4}/);
163
+
164
+ const latest = frames(session).at(-1);
165
+ // The projection carries the REFERENCE and nothing else — that is why
166
+ // `supportView` exists rather than `syncState` taking the state itself.
167
+ expect(latest?.ticket).toMatch(/^TCK\d{4}$/);
168
+ for (const frame of frames(session)) {
169
+ expect(JSON.stringify(frame)).not.toMatch(/900123/);
170
+ }
171
+ },
172
+ {
173
+ stubReply: [
174
+ { tool: "answer_question", args: { question: "add a mobile SIM to my account" } },
175
+ "I don't have that documented. Shall I log a ticket for a callback?",
176
+ {
177
+ tool: "log_ticket",
178
+ args: { question: "add a mobile SIM to my account", callback: "07700 900123" },
179
+ },
180
+ "That's logged — the reference is T C K four thousand and one.",
181
+ ],
182
+ },
183
+ );
184
+
185
+ test(
186
+ "the answer comes from the document the caller asked about, not the one next to it",
187
+ async ({ session }) => {
188
+ // Short and single, deliberately: the claim here is which document the
189
+ // grader passed, so the question is phrased to measure that and not the
190
+ // loop's pacing.
191
+ //
192
+ // It used to say the corrective path could not fit the runtime's 30s
193
+ // per-tool deadline. Re-measured against the live gateway, that is no
194
+ // longer true by an order of magnitude — the two-attempt path runs in
195
+ // 2.8s of nine model calls — so the phrasing above is a choice about what
196
+ // this case measures rather than a way around a clock. The numbers and
197
+ // what they do and do not license are on `LOOKUP_BUDGET_MS` in
198
+ // `tools/answer_question.ts`.
199
+ const turn = await session.say("How much notice do I have to give to cancel my contract?");
200
+
201
+ const [payload] = lookups(session);
202
+ expect(
203
+ payload,
204
+ `tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
205
+ ).toBeDefined();
206
+ // The whole verdict in the message: `grounded: undefined` on its own does
207
+ // not say whether the lookup failed, or ran and refused.
208
+ const verdict = JSON.stringify(payload);
209
+ expect(payload?.grounded, verdict).toBe(true);
210
+ expect(payload?.answersTheQuestion, verdict).toBe(true);
211
+
212
+ const sources = payload?.sources ?? [];
213
+ expect(sources).toContain("Cancelling your contract");
214
+ // THE claim, and the reason the knowledge base is built the way it is:
215
+ // "cancelling your contract" and "cancelling an engineer visit" are two
216
+ // documents, two fees and one word apart. A retriever with no grader hands
217
+ // back both, and sixty pounds gets read down the phone as the answer to a
218
+ // contract question.
219
+ expect(sources).not.toContain("Cancelling or rebooking an engineer visit");
220
+ // Either spelling: the claim is that the notice period came out of D5, not
221
+ // how the answer prompt chose to write a numeral.
222
+ expect(String(payload?.answer)).toMatch(/thirty days|30 days/i);
223
+ expect(String(payload?.answer)).not.toMatch(/sixty pound|abortive/i);
224
+ expect(turn.text).not.toMatch(/sixty|abortive/i);
225
+ },
226
+ // Live only: the grader's verdict IS the measurement.
227
+ //
228
+ // THE CLOCK, and it is no longer the limit it was recorded as here.
229
+ // `answer_question` is up to eleven sequential model calls and the tool
230
+ // executor abandons any tool at `TOOL_EXECUTION_TIMEOUT_MS` (30s) — a
231
+ // deadline neither an agent author nor an eval can raise — so this comment
232
+ // used to say the two-attempt corrective path did not fit, which made this
233
+ // case a coin toss on the gateway rather than a reading of the grader.
234
+ // Re-measured: the whole loop is 2.0-3.1s, the corrective path 2.8s of nine
235
+ // calls, and eight consecutive live runs of this suite were green. The
236
+ // margin belongs to the service, not to us, so the tool keeps its own
237
+ // budget and this case keeps a question that settles on one attempt; if the
238
+ // gateway slows down again the failure says which, because a spent budget
239
+ // comes back as the graph's own exit sentence rather than as a timeout.
240
+ { live: true },
241
+ );
242
+
243
+ test(
244
+ "a question nothing covers is refused, after the corrective edge has tried",
245
+ async ({ session }) => {
246
+ const turn = await session.say(
247
+ "Can I add a pay-monthly mobile SIM with unlimited data to my broadband account?",
248
+ );
249
+
250
+ const [payload] = lookups(session);
251
+ expect(
252
+ payload,
253
+ `tools called: [${turn.toolCalls.map((c) => c.name).join(", ")}]; said: ${turn.text}`,
254
+ ).toBeDefined();
255
+ // Withheld, not softened: `answer: null` is the tool refusing to hand the
256
+ // model something to read out, and the guidance is the exit the grading
257
+ // apparatus needs — a support line that can only answer will answer wrong.
258
+ // A lookup that BROKE and one that ran and found nothing are different
259
+ // findings, and neither is readable from "answer is not null".
260
+ expect(payload?.answer, JSON.stringify(payload)).toBeNull();
261
+ expect(String(payload?.guidance)).toMatch(/log_ticket/);
262
+ // The corrective edge really ran: nothing relevant means the RETRIEVAL was
263
+ // wrong, so the query is rewritten and retried before the loop gives up.
264
+ expect(payload?.rewrites).toBeGreaterThanOrEqual(1);
265
+
266
+ const trace = frames(session).at(-1)?.trace;
267
+ expect(trace?.exhausted).toBe(true);
268
+ expect(trace?.docs.every((doc) => !doc.relevant)).toBe(true);
269
+ expect(trace?.steps.map((step) => step.node)).toContain("transform_query");
270
+ },
271
+ { live: true },
272
+ );
273
+ });