@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,444 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * The flow: watch some podcasts, transcribe what is new, summarize it, and post
4
+ * a digest to Slack — then do it again on a schedule, for as long as asked.
5
+ *
6
+ * ```text
7
+ * discoverEpisodes one step — links in, episodes with audio out
8
+ * submitTranscript N steps — hand each episode to AssemblyAI
9
+ * waitForTranscripts the BODY — poll the whole batch, sleeping between rounds
10
+ * summarizeTranscript N steps — one model call per finished transcript
11
+ * sendDigestToSlack one step — the message
12
+ * sleep(interval) the BODY — and around again
13
+ * ```
14
+ *
15
+ * ## What this template is FOR
16
+ *
17
+ * The other workflow templates are one-shot: something arrives, a run processes
18
+ * it, the run ends. This one is the shape nothing else here demonstrates — a
19
+ * run that is **long-lived and periodic**. It sleeps for days at a time and
20
+ * wakes up to do the same work again, which is only possible because a durable
21
+ * sleep is suspension rather than waiting: nothing is resident, nothing is
22
+ * billed, and the run survives the agent restarting, redeploying or going idle
23
+ * underneath it.
24
+ *
25
+ * That single fact is why `daysToRun` exists as an input at all. A run that
26
+ * repeats forever is a resource nobody can see and nobody remembers to cancel,
27
+ * so this one is asked up front how many digests it owes and then finishes.
28
+ *
29
+ * ## Storage is not optional here, unlike everywhere else
30
+ *
31
+ * Every other template says "enable the database for durability, but it is fine
32
+ * to build without it". For this one it is closer to a requirement: without
33
+ * storage the run lives in the process, and a process does not survive a
34
+ * multi-day sleep. Set `DATABASE_URL` under `aai dev`, or `aai storage enable`
35
+ * once deployed. Build it with `intervalUnit: "minutes"` and you will see it
36
+ * work either way; leave it on `days` without storage and the second digest
37
+ * never arrives.
38
+ *
39
+ * ## Batch polling, which is this file's one genuinely new mechanism
40
+ *
41
+ * `spoken-summary` and `transcription-workflow` each wait for ONE transcript,
42
+ * so their poll loop is `for (…) { if (done) return; await sleep(…) }`. Here N
43
+ * episodes are in flight at once and they finish out of order, so the loop has
44
+ * to carry a shrinking pending set and let the finished ones drop out — see
45
+ * {@link waitForTranscripts}. It is the same idea one dimension up, and the
46
+ * reason it stays in the template rather than moving to the SDK is that the SDK
47
+ * owns what is INSIDE a step and never the body's control flow.
48
+ *
49
+ * @module digest
50
+ */
51
+
52
+ import { mapConcurrent, report, TRANSCRIBE_API } from "@alexkroman1/aai/step";
53
+ import {
54
+ stepGenerateJsonClassified,
55
+ stepTranscribePollClassified,
56
+ stepTranscribeSubmitClassified,
57
+ } from "@alexkroman1/aai/step-errors";
58
+ import { errorMessage } from "@alexkroman1/aai/utils";
59
+ import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
60
+ import { FatalError, sleep } from "workflow";
61
+ import { z } from "zod";
62
+ import type { dailyDigest } from "../agent.ts";
63
+ import { discoverEpisodes, type Episode } from "./feeds.ts";
64
+ import { sendDigestToSlack } from "./slack.ts";
65
+
66
+ /**
67
+ * Between polling rounds. Transcription is minutes, so this is not a busy wait.
68
+ *
69
+ * Exported so `agent.eval.test.ts` can assert on the wait a run RECORDED without
70
+ * restating the number — an eval skips the sleep, so the duration asked for is
71
+ * the only observable there is.
72
+ */
73
+ export const POLL_DELAY = "20 seconds";
74
+
75
+ /**
76
+ * 180 rounds x 20s = an hour, which is far past any podcast episode.
77
+ *
78
+ * Exported for the same reason as {@link POLL_DELAY}: the eval drives a job past
79
+ * this budget and reads the degraded entry, and a restated 180 in two files is a
80
+ * second place for the number to be wrong.
81
+ */
82
+ export const MAX_POLL_ATTEMPTS = 180;
83
+
84
+ /**
85
+ * How much transcript the model reads.
86
+ *
87
+ * A three-hour episode is far more than a summary needs and more than the
88
+ * context window wants to pay for. Truncating is the right call for a DIGEST
89
+ * specifically: the opening of an episode is where its subject is stated.
90
+ */
91
+ const MAX_TRANSCRIPT_CHARS = 18_000;
92
+
93
+ /** Submissions in flight at once — polite to the provider, still parallel. */
94
+ const SUBMIT_CONCURRENCY = 2;
95
+
96
+ /** Status checks in flight at once. Cheaper calls, so a wider gate. */
97
+ const POLL_CONCURRENCY = 3;
98
+
99
+ /**
100
+ * What a run is started with — the schema's OUTPUT, so every `.default()` has
101
+ * already run and nothing here is optional.
102
+ *
103
+ * Derived rather than restated. The hand-written version this replaces declared
104
+ * six optional fields against a schema where all six carry a default, so the
105
+ * body re-applied every one of them with `??` and a `.default(5)` beside a
106
+ * `?? 3` could disagree with nothing reporting it.
107
+ */
108
+ export type DigestInput = WorkflowInputOf<typeof dailyDigest>;
109
+
110
+ export type IntervalUnit = "minutes" | "hours" | "days";
111
+
112
+ /**
113
+ * An episode on its way through transcription.
114
+ *
115
+ * A discriminated union rather than an optional `transcript` field, so the
116
+ * "we could not transcribe this one" case is a STATE the compiler makes every
117
+ * reader handle — not a `undefined` somebody forgets to check. One bad episode
118
+ * must not sink a digest of five.
119
+ */
120
+ type TranscriptJob = Episode &
121
+ (
122
+ | { transcriptStatus: "submitted"; transcriptId: string }
123
+ | { transcriptStatus: "unavailable"; reason: string }
124
+ );
125
+
126
+ type TranscriptState = Episode &
127
+ (
128
+ | { transcriptStatus: "done"; transcriptId: string; transcript: string; durationMs: number }
129
+ | { transcriptStatus: "unavailable"; reason: string }
130
+ );
131
+
132
+ /** One episode as it appears in the message and on the page. */
133
+ export type EpisodeDigest = Episode & {
134
+ transcriptSource: "assemblyai" | "unavailable";
135
+ summary: string;
136
+ keyPoints: string[];
137
+ };
138
+
139
+ export type DailyDigestOutput = {
140
+ podcastChannels: string;
141
+ deliveryTarget: string;
142
+ scheduleInterval: string;
143
+ digestsScheduled: number;
144
+ digestsSent: number;
145
+ lastDigest: {
146
+ sentAt: string;
147
+ slackStatus: string;
148
+ episodes: EpisodeDigest[];
149
+ } | null;
150
+ };
151
+
152
+ /** What the model must answer with, and what `stepGenerateJsonClassified` enforces. */
153
+ const SummaryReply = z.object({
154
+ summary: z.string().trim().min(1),
155
+ keyPoints: z.array(z.string().trim().min(1)).min(1).max(5),
156
+ });
157
+
158
+ /**
159
+ * The body.
160
+ *
161
+ * Everything it does is a step call or a `sleep`, which is what makes it legal
162
+ * to replay: the DevKit re-runs this function from the top after any crash, and
163
+ * each step it reaches is either replayed from the journal or executed for the
164
+ * first time. Nothing here reads a clock, generates an id, or touches anything
165
+ * that would answer differently on the second pass — {@link timestamp} is a step
166
+ * for exactly that reason.
167
+ */
168
+ export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOutput> {
169
+ "use workflow";
170
+
171
+ // No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
172
+ // `.default()` has already run by the time a run reaches this line. The
173
+ // chain this replaces restated all four of them, which is a second place for
174
+ // the number to be wrong.
175
+ const { daysToRun: totalDigests, maxEpisodesPerDigest: maxEpisodes } = input;
176
+ const { intervalEvery, intervalUnit } = input;
177
+ const intervalMs = scheduleIntervalMs(intervalEvery, intervalUnit);
178
+
179
+ let lastDigest: DailyDigestOutput["lastDigest"] = null;
180
+ let digestsSent = 0;
181
+
182
+ for (let digestNumber = 1; digestNumber <= totalDigests; digestNumber += 1) {
183
+ const episodes = await discoverEpisodes(input.podcastChannels, maxEpisodes);
184
+ const jobs = await mapConcurrent(episodes, SUBMIT_CONCURRENCY, submitTranscript);
185
+ const transcripts = await waitForTranscripts(jobs);
186
+ const digests = await mapConcurrent(transcripts, SUBMIT_CONCURRENCY, summarizeTranscript);
187
+
188
+ const slackStatus = await sendDigestToSlack({
189
+ slackWebhookUrl: input.slackWebhookUrl,
190
+ slackWorkflowTextParam: input.slackWorkflowTextParam,
191
+ podcastChannels: input.podcastChannels,
192
+ episodes: digests,
193
+ digestNumber,
194
+ totalDigests,
195
+ });
196
+
197
+ lastDigest = { sentAt: await timestamp(), slackStatus, episodes: digests };
198
+ digestsSent += 1;
199
+
200
+ // Not after the last one: a run that has delivered everything it owes
201
+ // should end, not sleep for a day and then end.
202
+ if (digestNumber < totalDigests) await sleep(intervalMs);
203
+ }
204
+
205
+ return {
206
+ podcastChannels: input.podcastChannels,
207
+ deliveryTarget: "Slack webhook",
208
+ scheduleInterval: formatScheduleInterval(intervalEvery, intervalUnit),
209
+ digestsScheduled: totalDigests,
210
+ digestsSent,
211
+ lastDigest,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Wait for a whole BATCH of transcripts, letting them finish out of order.
217
+ *
218
+ * A plain async function rather than a step, and not because it is small: it
219
+ * calls steps and it `sleep`s, neither of which a step may do. So it runs as
220
+ * part of the body and is replayed with it — legal for the ordinary reason,
221
+ * that every line is either a step call or a `sleep`.
222
+ *
223
+ * The shape to notice is that `pending` SHRINKS. A loop that waited for all N
224
+ * on every round would hold the whole batch hostage to its slowest member, and
225
+ * with `maxEpisodesPerDigest` up to 20 that is the difference between a digest
226
+ * arriving and a digest timing out. Finished episodes move to `completed` and
227
+ * are never polled again.
228
+ *
229
+ * Running out of rounds is NOT an error. An episode nobody could transcribe in
230
+ * an hour becomes `unavailable` with a reason, and the digest goes out with the
231
+ * other four — a partial digest being obviously better than none, and the
232
+ * reason being printed where a reader will see it.
233
+ *
234
+ * **What comes back is in the order it went in**, which is not what a first
235
+ * draft does. Appending each episode as it finishes — and concatenating the
236
+ * ones that never did on the end — makes the digest's running order a report of
237
+ * TRANSCRIPTION LATENCY: `discoverEpisodes` sorted the feed newest first, and a
238
+ * reader then sees whichever episode the provider happened to finish first at
239
+ * the top. Found by `agent.eval.test.ts`, which is the only tier that can see
240
+ * it, a per-step spec having no batch to order.
241
+ */
242
+ async function waitForTranscripts(jobs: TranscriptJob[]): Promise<TranscriptState[]> {
243
+ let pending = jobs;
244
+ // Keyed by episode id rather than appended, and that is what keeps the digest
245
+ // in PUBLICATION order — see this function's doc.
246
+ const settled = new Map<string, TranscriptState>();
247
+
248
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS && pending.length > 0; attempt += 1) {
249
+ const polled = await mapConcurrent(pending, POLL_CONCURRENCY, pollTranscript);
250
+ for (const state of polled) {
251
+ if (state.transcriptStatus !== "submitted") settled.set(state.id, state);
252
+ }
253
+ pending = polled.filter((state) => state.transcriptStatus === "submitted");
254
+ if (pending.length > 0) await sleep(POLL_DELAY);
255
+ }
256
+
257
+ for (const job of pending) settled.set(job.id, gaveUpOn(job));
258
+ // The list `jobs` arrived in, which `discoverEpisodes` sorted newest first.
259
+ return jobs.map((job) => settled.get(job.id) ?? gaveUpOn(job));
260
+ }
261
+
262
+ /**
263
+ * One episode the run is done waiting for.
264
+ *
265
+ * Its own function because {@link waitForTranscripts} needs it in two places —
266
+ * the jobs still pending when the budget ran out, and the unreachable fallback
267
+ * the ordered rebuild above needs for a lookup that cannot miss.
268
+ */
269
+ function gaveUpOn(job: TranscriptJob): TranscriptState {
270
+ return {
271
+ ...job,
272
+ transcriptStatus: "unavailable" as const,
273
+ reason:
274
+ job.transcriptStatus === "submitted"
275
+ ? // The transcript is not lost — it is still on the provider, and the
276
+ // message says where, the same way the other transcription templates do.
277
+ `Transcript ${job.transcriptId} was still unfinished after ${MAX_POLL_ATTEMPTS} ` +
278
+ `checks. It is not lost — read it with GET ${TRANSCRIBE_API}/v2/transcript/${job.transcriptId}.`
279
+ : job.reason,
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Hand one episode's audio to AssemblyAI.
285
+ *
286
+ * The `catch` is the interesting line, and the whole partial-failure policy is
287
+ * in it: transport problems retry, this-episode-is-broken problems degrade. A
288
+ * terminal failure — a 404 on the media URL, a file that is not audio — becomes
289
+ * an `unavailable` VALUE rather than a throw, because one bad episode must not
290
+ * sink a digest of five.
291
+ *
292
+ * The verdict itself is the SDK's: `stepTranscribeSubmitClassified` reads
293
+ * `TranscribeError`'s own `retryable` AND its `retryAfter`, and throws a
294
+ * `FatalError` or a `RetryableError` accordingly. The hand-written
295
+ * `err instanceof TranscribeError && err.retryable` this replaces read only the
296
+ * first of those, so a provider that named a delay was retried on the DevKit's
297
+ * one-second default instead.
298
+ */
299
+ export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
300
+ "use step";
301
+
302
+ await report(`Submitting ${episode.title} for transcription.`);
303
+ try {
304
+ const { id } = await stepTranscribeSubmitClassified(episode.audioUrl, {
305
+ // A digest quotes nobody, so who spoke costs time for nothing.
306
+ params: { speaker_labels: false },
307
+ });
308
+ return { ...episode, transcriptStatus: "submitted", transcriptId: id };
309
+ } catch (err) {
310
+ if (!FatalError.is(err)) throw err;
311
+ return { ...episode, transcriptStatus: "unavailable", reason: errorMessage(err) };
312
+ }
313
+ }
314
+
315
+ submitTranscript.maxRetries = 4;
316
+
317
+ /**
318
+ * Ask once whether one job has finished.
319
+ *
320
+ * One poll is one step, so each round is journaled on its own: a run that dies
321
+ * mid-wait resumes knowing the last answer instead of re-transcribing. It
322
+ * returns the job UNCHANGED when the answer is "not yet", which is what lets
323
+ * {@link waitForTranscripts} keep it in `pending` without a second vocabulary
324
+ * for "still going".
325
+ */
326
+ export async function pollTranscript(job: TranscriptJob): Promise<TranscriptState | TranscriptJob> {
327
+ "use step";
328
+
329
+ if (job.transcriptStatus === "unavailable") return job;
330
+
331
+ try {
332
+ const progress = await stepTranscribePollClassified(job.transcriptId);
333
+ // Branch on `done`, never on a status string: a vocabulary this body does
334
+ // not own would otherwise read as "not finished yet" forever.
335
+ if (!progress.done) return job;
336
+
337
+ await report(`Transcribed ${job.title}.`);
338
+ return {
339
+ ...job,
340
+ transcriptStatus: "done",
341
+ transcriptId: job.transcriptId,
342
+ transcript: progress.transcript.text,
343
+ durationMs: progress.transcript.durationMs,
344
+ };
345
+ } catch (err) {
346
+ // Same policy as {@link submitTranscript}: the SDK classified it, a
347
+ // retryable verdict goes back to the DevKit, a terminal one degrades.
348
+ if (!FatalError.is(err)) throw err;
349
+ return { ...job, transcriptStatus: "unavailable", reason: errorMessage(err) };
350
+ }
351
+ }
352
+
353
+ pollTranscript.maxRetries = 4;
354
+
355
+ /** Reduce one transcript to the summary and points the digest carries. */
356
+ export async function summarizeTranscript(state: TranscriptState): Promise<EpisodeDigest> {
357
+ "use step";
358
+
359
+ if (state.transcriptStatus === "unavailable") {
360
+ // Still an entry in the digest. A reader who sees four summaries and one
361
+ // stated reason knows what happened; four summaries and silence looks like
362
+ // the feed simply had four episodes.
363
+ return {
364
+ ...episodeOf(state),
365
+ transcriptSource: "unavailable",
366
+ summary: `This episode could not be transcribed: ${state.reason}`,
367
+ keyPoints: ["No transcript was available to summarize."],
368
+ };
369
+ }
370
+
371
+ await report(`Summarizing ${state.title}.`);
372
+ const parsed = await stepGenerateJsonClassified(
373
+ [
374
+ `Podcast: ${state.podcastTitle}`,
375
+ `Episode: ${state.title}`,
376
+ `Published: ${state.published}`,
377
+ "",
378
+ "Transcript:",
379
+ state.transcript.slice(0, MAX_TRANSCRIPT_CHARS),
380
+ ].join("\n"),
381
+ {
382
+ schema: SummaryReply,
383
+ system:
384
+ "You summarize podcast transcripts for a daily digest. Reply with JSON only: " +
385
+ '{"summary": string, "keyPoints": string[]}. Keep the summary to a few sentences ' +
386
+ "and give 3 to 5 concrete key points — decisions, numbers, names, claims — never " +
387
+ '"the hosts discussed several topics".',
388
+ },
389
+ );
390
+
391
+ return {
392
+ ...episodeOf(state),
393
+ transcriptSource: "assemblyai",
394
+ summary: parsed.summary,
395
+ keyPoints: parsed.keyPoints,
396
+ };
397
+ }
398
+
399
+ summarizeTranscript.maxRetries = 5;
400
+
401
+ /**
402
+ * The clock, as a step.
403
+ *
404
+ * A step's result is journaled and therefore stable across replays, where
405
+ * `new Date()` in the body would answer differently on every one — and a body
406
+ * that is not deterministic is a body the DevKit cannot replay.
407
+ */
408
+ export async function timestamp(): Promise<string> {
409
+ "use step";
410
+ return new Date().toISOString();
411
+ }
412
+
413
+ // ---- Pure helpers -----------------------------------------------------------
414
+
415
+ /** The `Episode` half of a state, without its transcript fields. */
416
+ function episodeOf(state: Episode): Episode {
417
+ return {
418
+ id: state.id,
419
+ feedUrl: state.feedUrl,
420
+ podcastTitle: state.podcastTitle,
421
+ title: state.title,
422
+ url: state.url,
423
+ audioUrl: state.audioUrl,
424
+ published: state.published,
425
+ };
426
+ }
427
+
428
+ /** "1 hour", "15 minutes" — the schedule as the page prints it. */
429
+ export function formatScheduleInterval(every: number, unit: IntervalUnit): string {
430
+ return `${every} ${every === 1 ? unit.slice(0, -1) : unit}`;
431
+ }
432
+
433
+ /**
434
+ * The interval in milliseconds, which is one of the three things `sleep` takes.
435
+ *
436
+ * `sleep` also accepts a duration STRING (`"20 seconds"`, as `POLL_DELAY`
437
+ * above), and building one here would look tidier — but that overload is typed
438
+ * as a template-literal union, so a value assembled from two variables does not
439
+ * satisfy it without a cast. A number needs no cast and no trust.
440
+ */
441
+ export function scheduleIntervalMs(every: number, unit: IntervalUnit): number {
442
+ const multiplier = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000;
443
+ return every * multiplier;
444
+ }