@alexkroman1/aai-cli 6.10.1 → 7.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 (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +93 -14
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +13 -4
  28. package/dist/scaffold/vite.config.ts +1 -1
  29. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  30. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  31. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  32. package/dist/templates/call-audit/agent.test.ts +818 -0
  33. package/dist/templates/call-audit/agent.ts +171 -0
  34. package/dist/templates/call-audit/client.tsx +198 -0
  35. package/dist/templates/call-audit/workflows/audit.ts +298 -0
  36. package/dist/templates/call-audit/workflows/ingest.ts +251 -0
  37. package/dist/templates/call-audit/workflows/media.ts +637 -0
  38. package/dist/templates/call-audit/workflows/summarize.ts +201 -0
  39. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  40. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  41. package/dist/templates/dispatch-center/client.tsx +239 -129
  42. package/dist/templates/dispatch-center/shared.ts +99 -1
  43. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  44. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  45. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  46. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  47. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  48. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  49. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  50. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  51. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  52. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  53. package/dist/templates/health-assistant/agent.test.ts +22 -4
  54. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  55. package/dist/templates/infocom-adventure/client.tsx +246 -164
  56. package/dist/templates/link-digest/agent.test.ts +24 -19
  57. package/dist/templates/link-digest/client.tsx +47 -61
  58. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  59. package/dist/templates/night-owl/agent.test.ts +70 -19
  60. package/dist/templates/night-owl/agent.ts +5 -0
  61. package/dist/templates/night-owl/client.tsx +56 -56
  62. package/dist/templates/night-owl/shared.ts +24 -0
  63. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  64. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  65. package/dist/templates/pizza-ordering/client.tsx +9 -26
  66. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  67. package/dist/templates/plan-and-execute/agent.ts +1 -1
  68. package/dist/templates/plan-and-execute/client.tsx +12 -15
  69. package/dist/templates/plan-and-execute/shared.ts +71 -2
  70. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  71. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  72. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  73. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  74. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  75. package/dist/templates/podcast-digest/agent.ts +139 -0
  76. package/dist/templates/podcast-digest/client.tsx +154 -0
  77. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  78. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  79. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  80. package/dist/templates/recap-workflow/agent.test.ts +107 -96
  81. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  82. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  83. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  84. package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
  85. package/dist/templates/redline/agent.test.ts +17 -15
  86. package/dist/templates/redline/client.tsx +12 -12
  87. package/dist/templates/redline/workflows/redline.ts +19 -31
  88. package/dist/templates/research-workflow/agent.test.ts +60 -59
  89. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  90. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  91. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  92. package/dist/templates/retail/agent.test.ts +26 -23
  93. package/dist/templates/retail/client.tsx +226 -117
  94. package/dist/templates/retail/registry.test.ts +38 -6
  95. package/dist/templates/retail/store.test.ts +82 -15
  96. package/dist/templates/retail/store.ts +174 -47
  97. package/dist/templates/retail/system-prompt.md +11 -2
  98. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  99. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  100. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  101. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  102. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  105. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  106. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  107. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  108. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  109. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  110. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  111. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  112. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  113. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  114. package/dist/templates/solo-rpg/client.tsx +38 -37
  115. package/dist/templates/solo-rpg/shared.ts +145 -19
  116. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  117. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  118. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  119. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  120. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  121. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  122. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  123. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  124. package/dist/templates/spoken-summary/agent.test.ts +314 -0
  125. package/dist/templates/spoken-summary/agent.ts +155 -0
  126. package/dist/templates/spoken-summary/client.tsx +185 -0
  127. package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
  128. package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
  129. package/dist/templates/support-line/agent.test.ts +11 -16
  130. package/dist/templates/support-line/agent.ts +1 -1
  131. package/dist/templates/support-line/client.tsx +9 -9
  132. package/dist/templates/support-line/nodes.ts +100 -0
  133. package/dist/templates/support-line/procedure.ts +407 -0
  134. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  135. package/dist/templates/transcription-workflow/agent.test.ts +246 -123
  136. package/dist/templates/transcription-workflow/agent.ts +21 -7
  137. package/dist/templates/transcription-workflow/client.tsx +17 -42
  138. package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
  139. package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
  140. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  141. package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
  142. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  143. package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
  144. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
  145. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  146. package/dist/templates/travel-concierge/client.tsx +11 -23
  147. package/dist/templates/travel-concierge/routing.ts +34 -15
  148. package/dist/templates/travel-concierge/shared.ts +70 -3
  149. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  153. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  154. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  155. package/dist/worker-bundler.mjs +1 -1
  156. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  157. package/dist/workflow-bundler.d.ts +6 -1
  158. package/dist/workflow.d.ts +1 -1
  159. package/package.json +5 -4
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -0,0 +1,411 @@
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
+ /** Between polling rounds. Transcription is minutes, so this is not a busy wait. */
67
+ const POLL_DELAY = "20 seconds";
68
+
69
+ /** 180 rounds x 20s = an hour, which is far past any podcast episode. */
70
+ const MAX_POLL_ATTEMPTS = 180;
71
+
72
+ /**
73
+ * How much transcript the model reads.
74
+ *
75
+ * A three-hour episode is far more than a summary needs and more than the
76
+ * context window wants to pay for. Truncating is the right call for a DIGEST
77
+ * specifically: the opening of an episode is where its subject is stated.
78
+ */
79
+ const MAX_TRANSCRIPT_CHARS = 18_000;
80
+
81
+ /** Submissions in flight at once — polite to the provider, still parallel. */
82
+ const SUBMIT_CONCURRENCY = 2;
83
+
84
+ /** Status checks in flight at once. Cheaper calls, so a wider gate. */
85
+ const POLL_CONCURRENCY = 3;
86
+
87
+ /**
88
+ * What a run is started with — the schema's OUTPUT, so every `.default()` has
89
+ * already run and nothing here is optional.
90
+ *
91
+ * Derived rather than restated. The hand-written version this replaces declared
92
+ * six optional fields against a schema where all six carry a default, so the
93
+ * body re-applied every one of them with `??` and a `.default(5)` beside a
94
+ * `?? 3` could disagree with nothing reporting it.
95
+ */
96
+ export type DigestInput = WorkflowInputOf<typeof dailyDigest>;
97
+
98
+ export type IntervalUnit = "minutes" | "hours" | "days";
99
+
100
+ /**
101
+ * An episode on its way through transcription.
102
+ *
103
+ * A discriminated union rather than an optional `transcript` field, so the
104
+ * "we could not transcribe this one" case is a STATE the compiler makes every
105
+ * reader handle — not a `undefined` somebody forgets to check. One bad episode
106
+ * must not sink a digest of five.
107
+ */
108
+ type TranscriptJob = Episode &
109
+ (
110
+ | { transcriptStatus: "submitted"; transcriptId: string }
111
+ | { transcriptStatus: "unavailable"; reason: string }
112
+ );
113
+
114
+ type TranscriptState = Episode &
115
+ (
116
+ | { transcriptStatus: "done"; transcriptId: string; transcript: string; durationMs: number }
117
+ | { transcriptStatus: "unavailable"; reason: string }
118
+ );
119
+
120
+ /** One episode as it appears in the message and on the page. */
121
+ export type EpisodeDigest = Episode & {
122
+ transcriptSource: "assemblyai" | "unavailable";
123
+ summary: string;
124
+ keyPoints: string[];
125
+ };
126
+
127
+ export type DailyDigestOutput = {
128
+ podcastChannels: string;
129
+ deliveryTarget: string;
130
+ scheduleInterval: string;
131
+ digestsScheduled: number;
132
+ digestsSent: number;
133
+ lastDigest: {
134
+ sentAt: string;
135
+ slackStatus: string;
136
+ episodes: EpisodeDigest[];
137
+ } | null;
138
+ };
139
+
140
+ /** What the model must answer with, and what `stepGenerateJsonClassified` enforces. */
141
+ const SummaryReply = z.object({
142
+ summary: z.string().trim().min(1),
143
+ keyPoints: z.array(z.string().trim().min(1)).min(1).max(5),
144
+ });
145
+
146
+ /**
147
+ * The body.
148
+ *
149
+ * Everything it does is a step call or a `sleep`, which is what makes it legal
150
+ * to replay: the DevKit re-runs this function from the top after any crash, and
151
+ * each step it reaches is either replayed from the journal or executed for the
152
+ * first time. Nothing here reads a clock, generates an id, or touches anything
153
+ * that would answer differently on the second pass — {@link timestamp} is a step
154
+ * for exactly that reason.
155
+ */
156
+ export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOutput> {
157
+ "use workflow";
158
+
159
+ // No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
160
+ // `.default()` has already run by the time a run reaches this line. The
161
+ // chain this replaces restated all four of them, which is a second place for
162
+ // the number to be wrong.
163
+ const { daysToRun: totalDigests, maxEpisodesPerDigest: maxEpisodes } = input;
164
+ const { intervalEvery, intervalUnit } = input;
165
+ const intervalMs = scheduleIntervalMs(intervalEvery, intervalUnit);
166
+
167
+ let lastDigest: DailyDigestOutput["lastDigest"] = null;
168
+ let digestsSent = 0;
169
+
170
+ for (let digestNumber = 1; digestNumber <= totalDigests; digestNumber += 1) {
171
+ const episodes = await discoverEpisodes(input.podcastChannels, maxEpisodes);
172
+ const jobs = await mapConcurrent(episodes, SUBMIT_CONCURRENCY, submitTranscript);
173
+ const transcripts = await waitForTranscripts(jobs);
174
+ const digests = await mapConcurrent(transcripts, SUBMIT_CONCURRENCY, summarizeTranscript);
175
+
176
+ const slackStatus = await sendDigestToSlack({
177
+ slackWebhookUrl: input.slackWebhookUrl,
178
+ slackWorkflowTextParam: input.slackWorkflowTextParam,
179
+ podcastChannels: input.podcastChannels,
180
+ episodes: digests,
181
+ digestNumber,
182
+ totalDigests,
183
+ });
184
+
185
+ lastDigest = { sentAt: await timestamp(), slackStatus, episodes: digests };
186
+ digestsSent += 1;
187
+
188
+ // Not after the last one: a run that has delivered everything it owes
189
+ // should end, not sleep for a day and then end.
190
+ if (digestNumber < totalDigests) await sleep(intervalMs);
191
+ }
192
+
193
+ return {
194
+ podcastChannels: input.podcastChannels,
195
+ deliveryTarget: "Slack webhook",
196
+ scheduleInterval: formatScheduleInterval(intervalEvery, intervalUnit),
197
+ digestsScheduled: totalDigests,
198
+ digestsSent,
199
+ lastDigest,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Wait for a whole BATCH of transcripts, letting them finish out of order.
205
+ *
206
+ * A plain async function rather than a step, and not because it is small: it
207
+ * calls steps and it `sleep`s, neither of which a step may do. So it runs as
208
+ * part of the body and is replayed with it — legal for the ordinary reason,
209
+ * that every line is either a step call or a `sleep`.
210
+ *
211
+ * The shape to notice is that `pending` SHRINKS. A loop that waited for all N
212
+ * on every round would hold the whole batch hostage to its slowest member, and
213
+ * with `maxEpisodesPerDigest` up to 20 that is the difference between a digest
214
+ * arriving and a digest timing out. Finished episodes move to `completed` and
215
+ * are never polled again.
216
+ *
217
+ * Running out of rounds is NOT an error. An episode nobody could transcribe in
218
+ * an hour becomes `unavailable` with a reason, and the digest goes out with the
219
+ * other four — a partial digest being obviously better than none, and the
220
+ * reason being printed where a reader will see it.
221
+ */
222
+ async function waitForTranscripts(jobs: TranscriptJob[]): Promise<TranscriptState[]> {
223
+ let pending = jobs;
224
+ const completed: TranscriptState[] = [];
225
+
226
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS && pending.length > 0; attempt += 1) {
227
+ const polled = await mapConcurrent(pending, POLL_CONCURRENCY, pollTranscript);
228
+ for (const state of polled) {
229
+ if (state.transcriptStatus !== "submitted") completed.push(state);
230
+ }
231
+ pending = polled.filter((state) => state.transcriptStatus === "submitted");
232
+ if (pending.length > 0) await sleep(POLL_DELAY);
233
+ }
234
+
235
+ return completed.concat(
236
+ pending.map((job) => ({
237
+ ...job,
238
+ transcriptStatus: "unavailable" as const,
239
+ reason:
240
+ job.transcriptStatus === "submitted"
241
+ ? // The transcript is not lost — it is still on the provider, and the
242
+ // message says where, the same way the other transcription templates do.
243
+ `Transcript ${job.transcriptId} was still unfinished after ${MAX_POLL_ATTEMPTS} ` +
244
+ `checks. It is not lost — read it with GET ${TRANSCRIBE_API}/v2/transcript/${job.transcriptId}.`
245
+ : job.reason,
246
+ })),
247
+ );
248
+ }
249
+
250
+ /**
251
+ * Hand one episode's audio to AssemblyAI.
252
+ *
253
+ * The `catch` is the interesting line, and the whole partial-failure policy is
254
+ * in it: transport problems retry, this-episode-is-broken problems degrade. A
255
+ * terminal failure — a 404 on the media URL, a file that is not audio — becomes
256
+ * an `unavailable` VALUE rather than a throw, because one bad episode must not
257
+ * sink a digest of five.
258
+ *
259
+ * The verdict itself is the SDK's: `stepTranscribeSubmitClassified` reads
260
+ * `TranscribeError`'s own `retryable` AND its `retryAfter`, and throws a
261
+ * `FatalError` or a `RetryableError` accordingly. The hand-written
262
+ * `err instanceof TranscribeError && err.retryable` this replaces read only the
263
+ * first of those, so a provider that named a delay was retried on the DevKit's
264
+ * one-second default instead.
265
+ */
266
+ export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
267
+ "use step";
268
+
269
+ await report(`Submitting ${episode.title} for transcription.`);
270
+ try {
271
+ const { id } = await stepTranscribeSubmitClassified(episode.audioUrl, {
272
+ // A digest quotes nobody, so who spoke costs time for nothing.
273
+ params: { speaker_labels: false },
274
+ });
275
+ return { ...episode, transcriptStatus: "submitted", transcriptId: id };
276
+ } catch (err) {
277
+ if (!FatalError.is(err)) throw err;
278
+ return { ...episode, transcriptStatus: "unavailable", reason: errorMessage(err) };
279
+ }
280
+ }
281
+
282
+ submitTranscript.maxRetries = 4;
283
+
284
+ /**
285
+ * Ask once whether one job has finished.
286
+ *
287
+ * One poll is one step, so each round is journaled on its own: a run that dies
288
+ * mid-wait resumes knowing the last answer instead of re-transcribing. It
289
+ * returns the job UNCHANGED when the answer is "not yet", which is what lets
290
+ * {@link waitForTranscripts} keep it in `pending` without a second vocabulary
291
+ * for "still going".
292
+ */
293
+ export async function pollTranscript(job: TranscriptJob): Promise<TranscriptState | TranscriptJob> {
294
+ "use step";
295
+
296
+ if (job.transcriptStatus === "unavailable") return job;
297
+
298
+ try {
299
+ const progress = await stepTranscribePollClassified(job.transcriptId);
300
+ // Branch on `done`, never on a status string: a vocabulary this body does
301
+ // not own would otherwise read as "not finished yet" forever.
302
+ if (!progress.done) return job;
303
+
304
+ await report(`Transcribed ${job.title}.`);
305
+ return {
306
+ ...job,
307
+ transcriptStatus: "done",
308
+ transcriptId: job.transcriptId,
309
+ transcript: progress.transcript.text,
310
+ durationMs: progress.transcript.durationMs,
311
+ };
312
+ } catch (err) {
313
+ // Same policy as {@link submitTranscript}: the SDK classified it, a
314
+ // retryable verdict goes back to the DevKit, a terminal one degrades.
315
+ if (!FatalError.is(err)) throw err;
316
+ return { ...job, transcriptStatus: "unavailable", reason: errorMessage(err) };
317
+ }
318
+ }
319
+
320
+ pollTranscript.maxRetries = 4;
321
+
322
+ /** Reduce one transcript to the summary and points the digest carries. */
323
+ export async function summarizeTranscript(state: TranscriptState): Promise<EpisodeDigest> {
324
+ "use step";
325
+
326
+ if (state.transcriptStatus === "unavailable") {
327
+ // Still an entry in the digest. A reader who sees four summaries and one
328
+ // stated reason knows what happened; four summaries and silence looks like
329
+ // the feed simply had four episodes.
330
+ return {
331
+ ...episodeOf(state),
332
+ transcriptSource: "unavailable",
333
+ summary: `This episode could not be transcribed: ${state.reason}`,
334
+ keyPoints: ["No transcript was available to summarize."],
335
+ };
336
+ }
337
+
338
+ await report(`Summarizing ${state.title}.`);
339
+ const parsed = await stepGenerateJsonClassified(
340
+ [
341
+ `Podcast: ${state.podcastTitle}`,
342
+ `Episode: ${state.title}`,
343
+ `Published: ${state.published}`,
344
+ "",
345
+ "Transcript:",
346
+ state.transcript.slice(0, MAX_TRANSCRIPT_CHARS),
347
+ ].join("\n"),
348
+ {
349
+ schema: SummaryReply,
350
+ system:
351
+ "You summarize podcast transcripts for a daily digest. Reply with JSON only: " +
352
+ '{"summary": string, "keyPoints": string[]}. Keep the summary to a few sentences ' +
353
+ "and give 3 to 5 concrete key points — decisions, numbers, names, claims — never " +
354
+ '"the hosts discussed several topics".',
355
+ },
356
+ );
357
+
358
+ return {
359
+ ...episodeOf(state),
360
+ transcriptSource: "assemblyai",
361
+ summary: parsed.summary,
362
+ keyPoints: parsed.keyPoints,
363
+ };
364
+ }
365
+
366
+ summarizeTranscript.maxRetries = 5;
367
+
368
+ /**
369
+ * The clock, as a step.
370
+ *
371
+ * A step's result is journaled and therefore stable across replays, where
372
+ * `new Date()` in the body would answer differently on every one — and a body
373
+ * that is not deterministic is a body the DevKit cannot replay.
374
+ */
375
+ export async function timestamp(): Promise<string> {
376
+ "use step";
377
+ return new Date().toISOString();
378
+ }
379
+
380
+ // ---- Pure helpers -----------------------------------------------------------
381
+
382
+ /** The `Episode` half of a state, without its transcript fields. */
383
+ function episodeOf(state: Episode): Episode {
384
+ return {
385
+ id: state.id,
386
+ feedUrl: state.feedUrl,
387
+ podcastTitle: state.podcastTitle,
388
+ title: state.title,
389
+ url: state.url,
390
+ audioUrl: state.audioUrl,
391
+ published: state.published,
392
+ };
393
+ }
394
+
395
+ /** "1 hour", "15 minutes" — the schedule as the page prints it. */
396
+ export function formatScheduleInterval(every: number, unit: IntervalUnit): string {
397
+ return `${every} ${every === 1 ? unit.slice(0, -1) : unit}`;
398
+ }
399
+
400
+ /**
401
+ * The interval in milliseconds, which is one of the three things `sleep` takes.
402
+ *
403
+ * `sleep` also accepts a duration STRING (`"20 seconds"`, as `POLL_DELAY`
404
+ * above), and building one here would look tidier — but that overload is typed
405
+ * as a template-literal union, so a value assembled from two variables does not
406
+ * satisfy it without a cast. A number needs no cast and no trust.
407
+ */
408
+ export function scheduleIntervalMs(every: number, unit: IntervalUnit): number {
409
+ const multiplier = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000;
410
+ return every * multiplier;
411
+ }