@alexkroman1/aai-cli 9.1.0 → 10.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 (81) hide show
  1. package/dist/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
  2. package/dist/_bundler.d.ts +3 -41
  3. package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
  4. package/dist/_dev-server.d.ts +1 -8
  5. package/dist/_dev-vite-config.d.ts +21 -0
  6. package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
  7. package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
  8. package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
  9. package/dist/_templates.d.ts +29 -0
  10. package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
  11. package/dist/cli.mjs +46 -22
  12. package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
  13. package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
  14. package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
  15. package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
  16. package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
  17. package/dist/scaffold/CLAUDE.md +162 -54
  18. package/dist/scaffold/package.json +4 -6
  19. package/dist/scaffold/server.mjs +12 -23
  20. package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
  21. package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
  22. package/dist/templates/call-audit/agent.eval.test.ts +1 -1
  23. package/dist/templates/call-audit/agent.test.ts +80 -8
  24. package/dist/templates/call-audit/client.tsx +87 -3
  25. package/dist/templates/call-audit/workflows/audit.ts +52 -32
  26. package/dist/templates/call-audit/workflows/ingest.ts +15 -15
  27. package/dist/templates/call-audit/workflows/media.ts +1 -1
  28. package/dist/templates/call-audit/workflows/summarize.ts +0 -4
  29. package/dist/templates/link-digest/agent.eval.test.ts +4 -2
  30. package/dist/templates/link-digest/agent.test.ts +25 -5
  31. package/dist/templates/link-digest/client.tsx +65 -7
  32. package/dist/templates/link-digest/workflows/digest.ts +39 -29
  33. package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
  34. package/dist/templates/podcast-digest/agent.test.ts +108 -2
  35. package/dist/templates/podcast-digest/client.tsx +89 -8
  36. package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
  37. package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
  38. package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
  39. package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
  40. package/dist/templates/recap-workflow/agent.test.ts +323 -83
  41. package/dist/templates/recap-workflow/shared.ts +4 -3
  42. package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
  43. package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
  44. package/dist/templates/redline/agent.eval.test.ts +1 -1
  45. package/dist/templates/redline/agent.test.ts +1 -1
  46. package/dist/templates/redline/client.tsx +83 -3
  47. package/dist/templates/redline/workflows/redline.ts +26 -23
  48. package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
  49. package/dist/templates/research-workflow/agent.test.ts +32 -6
  50. package/dist/templates/research-workflow/agent.ts +4 -4
  51. package/dist/templates/research-workflow/shared.ts +4 -3
  52. package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
  53. package/dist/templates/research-workflow/workflows/research.ts +35 -38
  54. package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
  55. package/dist/templates/spoken-summary/agent.test.ts +10 -8
  56. package/dist/templates/spoken-summary/client.tsx +72 -3
  57. package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
  58. package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
  59. package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
  60. package/dist/templates/transcription-workflow/agent.test.ts +720 -16
  61. package/dist/templates/transcription-workflow/agent.ts +6 -4
  62. package/dist/templates/transcription-workflow/client.tsx +106 -308
  63. package/dist/templates/transcription-workflow/recover.test.ts +141 -0
  64. package/dist/templates/transcription-workflow/recover.ts +130 -0
  65. package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
  66. package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
  67. package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
  68. package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
  69. package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
  70. package/dist/templates/transcription-workflow/workflows/stream.ts +311 -38
  71. package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
  72. package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
  73. package/dist/worker-bundler.d.ts +0 -14
  74. package/dist/worker-bundler.mjs +149 -1
  75. package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
  76. package/dist/workflow.d.ts +1 -0
  77. package/package.json +4 -6
  78. package/dist/_workflow-scan.d.ts +0 -108
  79. package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
  80. package/dist/workflow-bundler.d.ts +0 -138
  81. package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
@@ -3,9 +3,8 @@
3
3
  * Specs for the spoken-summary app's declaration and its four legs.
4
4
  *
5
5
  * **The body itself is not driven here**, and that is a property of what a
6
- * workflow template demonstrates rather than a gap: imported through vitest
7
- * with no bundler in the path, a `"use step"` function is an ordinary async
8
- * function — so its HTTP handling, its fatal/retryable classification and what
6
+ * workflow template demonstrates rather than a gap: a step is an ordinary
7
+ * exported async function so its HTTP handling, its fatal/retryable classification and what
9
8
  * it returns are all testable, while durability, suspension and replay are not.
10
9
  * A body test that looked like a durability test would be the worse failure;
11
10
  * the real thing is exercised end to end by `aai-cli`'s
@@ -18,6 +17,8 @@
18
17
  */
19
18
 
20
19
  import { readUpload, uploadInfo } from "@alexkroman1/aai/step";
20
+ import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
21
+ import { createWorkflowCtx } from "@alexkroman1/aai/testing";
21
22
  import {
22
23
  installStubGateway,
23
24
  installStubReporter,
@@ -26,7 +27,6 @@ import {
26
27
  installStubUploads,
27
28
  } from "@alexkroman1/aai/testing/vitest";
28
29
  import { beforeEach, describe, expect, test, vi } from "vitest";
29
- import { FatalError, RetryableError } from "workflow";
30
30
  import agentDef, { spokenSummary } from "./agent.ts";
31
31
  import { speak, spokenSummaryFlow, summarize } from "./workflows/summarize.ts";
32
32
  import { createJob, pollTranscript, uploadToProvider } from "./workflows/transcribe.ts";
@@ -233,7 +233,7 @@ describe("the whole run", () => {
233
233
  /**
234
234
  * Answer every leg's HTTP, so the BODY can be driven end to end.
235
235
  *
236
- * Imported through vitest with no bundler in the path, a `"use workflow"`
236
+ * Imported through vitest, a workflow
237
237
  * function is an ordinary async function — so what this exercises is the
238
238
  * ORDER the legs are wired in and the shape they hand each other, which is
239
239
  * the one thing the per-leg specs above cannot see. Durability, suspension
@@ -276,7 +276,7 @@ describe("the whole run", () => {
276
276
  installStubReporter();
277
277
  installStubSpeech();
278
278
 
279
- const summary = await spokenSummaryFlow({ recording: UPLOAD_ID });
279
+ const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx());
280
280
 
281
281
  expect(summary).toEqual({
282
282
  source: "standup.wav",
@@ -298,7 +298,7 @@ describe("the whole run", () => {
298
298
  installStubReporter();
299
299
  const speech = installStubSpeech();
300
300
 
301
- await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" });
301
+ await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowCtx());
302
302
 
303
303
  expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
304
304
  });
@@ -308,6 +308,8 @@ describe("the whole run", () => {
308
308
  installStubReporter();
309
309
  installStubSpeech();
310
310
 
311
- await expect(spokenSummaryFlow({ recording: UPLOAD_ID })).rejects.toThrow("corrupt audio");
311
+ await expect(spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowCtx())).rejects.toThrow(
312
+ "corrupt audio",
313
+ );
312
314
  });
313
315
  });
@@ -30,6 +30,36 @@
30
30
  * so the file picker exists because `agent.ts` declares `recording` in
31
31
  * `uploads`, and the voice SELECT exists because it declares `voice` as an
32
32
  * enum. Adding a field there adds a control here with no edit.
33
+ *
34
+ * ## A reload used to lose a run whose input was already paid for
35
+ *
36
+ * A `runId` names a run for as long as something holds it, and this page held it
37
+ * in React state — so a refresh lost it while the recording carried on being
38
+ * transcribed, summarized and spoken. The bytes were already stored, so the
39
+ * expensive half had happened; what an empty form invites is somebody uploading
40
+ * the recording again and paying for all four legs twice. `key` is the handle
41
+ * that survives a reload, `recover: true` is what reads it back, and the upload
42
+ * half is already the SDK's — picking the same file again sends only the windows
43
+ * that did not land.
44
+ *
45
+ * **The key is `useRunKey()`** — opaque, and in `sessionStorage`, which is the
46
+ * same lifetime as that upload recall, so both halves of a reload make the same
47
+ * promise. What that rules out is a `?key=` parameter, and it is worth being
48
+ * plain about the trade because this template exists to produce something
49
+ * sendable: a summary you can listen to is exactly the sort of thing somebody
50
+ * forwards, and a URL is how people forward things. But there is no per-user
51
+ * filtering behind `find`, so the key IS the access control — a leaked one
52
+ * grants, on this agent, the whole TRANSCRIPT of the recording, the summary,
53
+ * the synthesized audio, and every other run filed under the same key. And URLs
54
+ * leak by ordinary use: chats, history, referrers. Sharing the artifact needs
55
+ * none of that — the page offers `Download summary.wav` and renders the words,
56
+ * so a person sends the file to exactly who they meant. Deriving the key from
57
+ * the recording would be worse again: two people summarizing one file would
58
+ * recover each other's runs.
59
+ *
60
+ * A real app with accounts passes the account's own id instead, and then a
61
+ * summary follows the person to another device — a promise only a login can
62
+ * keep.
33
63
  */
34
64
 
35
65
  import "@alexkroman1/aai-ui/styles.css";
@@ -44,10 +74,12 @@ import {
44
74
  SubmitButton,
45
75
  UploadProgressBar,
46
76
  useDownloadUrl,
77
+ useRunKey,
47
78
  useWorkflowSubmit,
48
79
  WorkflowFields,
49
80
  WorkflowProgress,
50
81
  } from "@alexkroman1/aai-ui";
82
+ import { useState } from "react";
51
83
  import type { spokenSummary } from "./agent.ts";
52
84
 
53
85
  /**
@@ -67,6 +99,20 @@ const WORKFLOW = "spokenSummary";
67
99
  */
68
100
  const api = createWorkflowApi();
69
101
 
102
+ /**
103
+ * What the page says while a run is in flight — three situations, one line
104
+ * each.
105
+ *
106
+ * The reload case gets its own words deliberately: somebody who did not press
107
+ * the button is owed an explanation for a summary appearing in front of them,
108
+ * and it is the line that keeps them from uploading the recording again.
109
+ */
110
+ function pendingNote(startedHere: boolean, found: boolean): string {
111
+ if (startedHere) return "Reloading is safe — this page will find the summary again.";
112
+ if (!found) return "Looking for a summary this tab started earlier…";
113
+ return "Still working on a recording this tab uploaded earlier — no need to send it again.";
114
+ }
115
+
70
116
  /**
71
117
  * The spoken text as a one-cue WebVTT track, inline.
72
118
  *
@@ -82,11 +128,20 @@ function captionsUrl(text: string, durationMs: number): string {
82
128
  }
83
129
 
84
130
  export function App() {
131
+ // This tab's handle on its own summaries, in the store the upload recall uses
132
+ // — see the module doc for why a `?key=` is the wrong trade here.
133
+ const key = useRunKey();
134
+ // Did THIS load start the run? A reload cannot have, and that is the only way
135
+ // the page can tell "working on what you just sent" from "picking up where
136
+ // you left off" — the hook reports the run, not who asked for it.
137
+ const [startedHere, setStartedHere] = useState(false);
85
138
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
86
- // `run.output` instead of `unknown`.
139
+ // `run.output` instead of `unknown`. Neither half of the recovery is useful
140
+ // alone: without `key` there is nothing to find the run by, and without
141
+ // `recover` the key is only ever written.
87
142
  const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
88
143
  typeof spokenSummary
89
- >(WORKFLOW, { api });
144
+ >(WORKFLOW, { api, key, recover: true });
90
145
  const output = run?.status === "completed" ? run.output : undefined;
91
146
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
92
147
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -102,7 +157,14 @@ export function App() {
102
157
  </p>
103
158
  </header>
104
159
 
105
- <Form onSubmit={submitForm} error={error} className="flex flex-col gap-4">
160
+ <Form
161
+ onSubmit={(values) => {
162
+ setStartedHere(true);
163
+ return submitForm(values);
164
+ }}
165
+ error={error}
166
+ className="flex flex-col gap-4"
167
+ >
106
168
  {/* Every control, from the workflow's own input schema. See the module doc. */}
107
169
  <WorkflowFields workflow={WORKFLOW} />
108
170
  <SubmitButton pending={pending} pendingLabel="Working…">
@@ -110,6 +172,13 @@ export function App() {
110
172
  </SubmitButton>
111
173
  </Form>
112
174
 
175
+ {/* `pending` covers the RUN rather than the request, and on a reload it is
176
+ also true while the run is being looked up by key — the stretch where
177
+ an empty form would invite a second upload of the same recording. */}
178
+ {pending && (
179
+ <p className="text-sm opacity-70">{pendingNote(startedHere, run !== undefined)}</p>
180
+ )}
181
+
113
182
  {/* The upload is its own wait, and the one nothing else can describe: the
114
183
  run does not EXIST until the bytes are in, so there is no run id and
115
184
  nothing for `<WorkflowProgress>` to read. */}
@@ -49,6 +49,7 @@
49
49
  * expensive one.
50
50
  */
51
51
 
52
+ import type { WorkflowCtx } from "@alexkroman1/aai";
52
53
  import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
53
54
  import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
54
55
  import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
@@ -56,13 +57,12 @@ import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
56
57
  // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
57
58
  // for `WorkflowOutputOf`.
58
59
  import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
59
- import { sleep } from "workflow";
60
60
  import { z } from "zod";
61
61
  import type { spokenSummary } from "../agent.ts";
62
62
  import {
63
63
  createJob,
64
64
  MAX_POLLS,
65
- POLL_INTERVAL,
65
+ POLL_INTERVAL_MS,
66
66
  pollTranscript,
67
67
  type Transcript,
68
68
  uploadToProvider,
@@ -128,12 +128,11 @@ export type SpokenSummary = {
128
128
  /** Transcribe a recording, summarize it, and read the summary back. */
129
129
  export async function spokenSummaryFlow(
130
130
  input: WorkflowInputOf<typeof spokenSummary>,
131
+ ctx: WorkflowCtx,
131
132
  ): Promise<SpokenSummary> {
132
- "use workflow";
133
-
134
- const transcript = await transcribe(input.recording);
135
- const summary = await summarize(transcript.text);
136
- const spoken = await speak(summary.spoken, input.voice);
133
+ const transcript = await transcribe(input.recording, ctx);
134
+ const summary = await ctx.step("summarize", () => summarize(transcript.text));
135
+ const spoken = await ctx.step("speak", () => speak(summary.spoken, input.voice));
137
136
 
138
137
  return {
139
138
  source: transcript.source,
@@ -151,20 +150,29 @@ export async function spokenSummaryFlow(
151
150
  /**
152
151
  * The whole first leg, factored out of the body.
153
152
  *
154
- * A plain async function rather than a step, and NOT because it is small: it
155
- * calls steps and it `sleep`s durably between polls, neither of which a step
156
- * may do. So it runs as part of the BODY and is replayed with it which is
157
- * legal here for the ordinary reason, that everything it does is either a step
158
- * call or a `sleep`, so a replay re-derives exactly the same sequence.
153
+ * Part of the BODY rather than a step, and NOT because it is small: it CALLS
154
+ * steps and it sleeps durably between polls, neither of which may happen inside
155
+ * one. So it is replayed with the body which is legal for the ordinary reason,
156
+ * that everything it does is either a `ctx.step` or a `ctx.sleep`, so a replay
157
+ * re-derives exactly the same sequence. It takes the `ctx` for that reason: a
158
+ * helper that reaches the journal has to be handed the handle.
159
159
  */
160
- async function transcribe(recording: string): Promise<Transcript> {
161
- const { audioUrl } = await uploadToProvider(recording);
162
- const job = await createJob(audioUrl);
160
+ async function transcribe(recording: string, ctx: WorkflowCtx): Promise<Transcript> {
161
+ // `maxAttempts: 6` was `uploadToProvider.maxRetries = 5` — five retries after
162
+ // the first attempt. It is the one step here worth extra patience: it streams
163
+ // the whole recording, so a transient failure is expensive to reach again.
164
+ const { audioUrl } = await ctx.step("uploadToProvider", () => uploadToProvider(recording), {
165
+ maxAttempts: 6,
166
+ });
167
+ const job = await ctx.step("createJob", () => createJob(audioUrl));
163
168
 
164
169
  for (let poll = 0; poll < MAX_POLLS; poll += 1) {
165
- const progress = await pollTranscript(recording, job.id);
170
+ // One call site in a loop, so each poll is its own journal entry
171
+ // (`pollTranscript#0`, `#1`, …) and a resume replays the polls already made
172
+ // rather than starting the wait over.
173
+ const progress = await ctx.step("pollTranscript", () => pollTranscript(recording, job.id));
166
174
  if (progress.done) return progress.transcript;
167
- await sleep(POLL_INTERVAL);
175
+ await ctx.sleep(POLL_INTERVAL_MS);
168
176
  }
169
177
  // A plain throw: this is the BODY, where the fatal/retryable distinction has
170
178
  // nothing to apply to. The transcript is not lost, so the message says where
@@ -179,8 +187,6 @@ async function transcribe(recording: string): Promise<Transcript> {
179
187
  export async function summarize(
180
188
  text: string,
181
189
  ): Promise<{ headline: string; points: string[]; spoken: string }> {
182
- "use step";
183
-
184
190
  await report("Summarizing the transcript.");
185
191
  const reply = await stepGenerateJsonClassified(
186
192
  "Summarize this transcript of a recording.\n\n" +
@@ -219,8 +225,6 @@ export async function speak(
219
225
  script: string,
220
226
  voice?: string,
221
227
  ): Promise<{ audio: string; durationMs: number }> {
222
- "use step";
223
-
224
228
  const spoken = await stepSpeak(script, omitUndefined({ voice }));
225
229
  const stored = await writeUpload(spoken.audio, {
226
230
  // Named, because this is what a person sees on the download link rather
@@ -22,12 +22,14 @@
22
22
  *
23
23
  * ## The steps are still OURS, and they have to be
24
24
  *
25
- * The SDK cannot ship a `"use step"`: the Workflow DevKit's builder transforms
26
- * exactly the files under this `workflows/` directory, so a directive inside a
27
- * dependency would be transformed by nothing and would run inline with no
28
- * journal and no retry, silently. The SDK owns what happens INSIDE a step; the
29
- * boundaries which is to say, what gets journaled and what a retry repeats —
30
- * are the app's.
25
+ * A step is a `ctx.step(name, fn)` in the BODY, and only the body holds a `ctx` —
26
+ * so the SDK could not declare one even if it wanted to. That is a better
27
+ * boundary than the one it replaced: under the DevKit a step directive shipped
28
+ * inside a dependency was transformed by nothing and ran inline with no journal
29
+ * and no retry, SILENTLY, and the rule was a convention this comment had to
30
+ * state. Now the SDK owns what happens INSIDE a step and the app owns which
31
+ * steps exist — which is to say what gets journaled and what a retry repeats —
32
+ * and neither can accidentally take the other's half.
31
33
  *
32
34
  * **The async API rather than the sync one, and the choice is about the FORM.**
33
35
  * The sync endpoint (`stepTranscribeSync`) answers inside the request and pays
@@ -46,13 +48,17 @@ import {
46
48
  } from "@alexkroman1/aai/step-errors";
47
49
  import { countWords, formatBytes } from "@alexkroman1/aai/utils";
48
50
 
49
- /** How long between polls of a submitted job. */
50
- export const POLL_INTERVAL = "10s";
51
+ /**
52
+ * How long between polls of a submitted job.
53
+ *
54
+ * Milliseconds: `ctx.sleep` takes a number or a `Date` and no duration STRING.
55
+ */
56
+ export const POLL_INTERVAL_MS = 10_000;
51
57
 
52
58
  /**
53
59
  * Polls before the run gives up on a job.
54
60
  *
55
- * At {@link POLL_INTERVAL} this is an hour, well past what the async API takes
61
+ * At {@link POLL_INTERVAL_MS} this is an hour, well past what the async API takes
56
62
  * for any recording it accepts. Bounded rather than endless because a job that
57
63
  * never leaves `queued` is a run that would otherwise be replayed forever.
58
64
  */
@@ -86,8 +92,6 @@ export type Transcript = {
86
92
  * same way for the same reason.
87
93
  */
88
94
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
89
- "use step";
90
-
91
95
  const stored = await uploadInfo(uploadId);
92
96
  await report(
93
97
  `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
@@ -95,13 +99,8 @@ export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: st
95
99
  return await stepTranscribeUploadClassified(uploadId);
96
100
  }
97
101
 
98
- /** Retries beyond the default 3: an upload is the one call here worth another attempt. */
99
- uploadToProvider.maxRetries = 5;
100
-
101
102
  /** Create the transcription job, and answer with the id that outlives this run. */
102
103
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
103
- "use step";
104
-
105
104
  const job = await stepTranscribeSubmitClassified(audioUrl);
106
105
  await report(`Transcribing — job ${job.id}.`);
107
106
  return job;
@@ -120,8 +119,6 @@ export async function pollTranscript(
120
119
  uploadId: string,
121
120
  id: string,
122
121
  ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
123
- "use step";
124
-
125
122
  const progress = await stepTranscribePollClassified(id);
126
123
  if (!progress.done) return { done: false };
127
124
 
@@ -24,7 +24,7 @@
24
24
  // that answers "is the transcript really of the recording".
25
25
  //
26
26
  // WHAT NO EVAL HERE COVERS: durability. Imported through vitest with no bundler
27
- // in the path, a `"use workflow"` body is an ordinary async function — no
27
+ // in the path, a workflow body is an ordinary async function — no
28
28
  // journal, no replay, and no per-step retry, so the resume-after-segment-27
29
29
  // property this template exists to demonstrate is NOT exercised here, and a
30
30
  // rate-limited live run fails where a deployed one would have ridden it out.