@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
@@ -151,7 +151,8 @@
151
151
  */
152
152
 
153
153
  import "@alexkroman1/aai-ui/styles.css";
154
- import type { WorkflowOutputOf } from "@alexkroman1/aai";
154
+ import { countWords, formatDuration, plural } from "@alexkroman1/aai/utils";
155
+ import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
155
156
  import {
156
157
  Form,
157
158
  isTerminal,
@@ -162,19 +163,14 @@ import {
162
163
  useWorkflowRuns,
163
164
  useWorkflowStream,
164
165
  useWorkflowSubmit,
166
+ WORKFLOW_STATUS_LABELS,
165
167
  WorkflowFields,
166
168
  WorkflowProgress,
167
169
  type WorkflowRun,
168
170
  } from "@alexkroman1/aai-ui";
169
171
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
170
172
  import type { transcribe } from "./agent.ts";
171
- import {
172
- clock,
173
- countWords,
174
- stitchChunks,
175
- TRANSCRIPT_STREAM,
176
- type TranscriptChunk,
177
- } from "./workflows/stitch.ts";
173
+ import { stitchChunks, TRANSCRIPT_STREAM, type TranscriptChunk } from "./workflows/stitch.ts";
178
174
 
179
175
  /**
180
176
  * What a finished run reports.
@@ -343,10 +339,10 @@ function TotalLatency({
343
339
  {running ? "Elapsed" : "Total latency"}
344
340
  </h2>
345
341
  <span className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
346
- <span className="text-sm tabular-nums">{duration(elapsedMs)}</span>
342
+ <span className="text-sm tabular-nums">{formatDuration(elapsedMs)}</span>
347
343
  {runMs !== undefined && outside !== undefined && (
348
344
  <span className="text-xs tabular-nums opacity-60">
349
- {duration(outside)} before the run · {duration(runMs)} inside it
345
+ {formatDuration(outside)} before the run · {formatDuration(runMs)} inside it
350
346
  </span>
351
347
  )}
352
348
  </span>
@@ -647,9 +643,9 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Transcript>; onClear?: ()
647
643
  {run.status === "completed" && (
648
644
  <>
649
645
  <p className="text-xs opacity-60">
650
- {run.output.segments} {run.output.segments === 1 ? "segment" : "segments"} ·{" "}
651
- {duration(run.output.durationMs)} of audio · took {duration(run.output.elapsedMs)} ·{" "}
652
- {run.output.words} words
646
+ {run.output.segments} {plural(run.output.segments, "segment")} ·{" "}
647
+ {formatDuration(run.output.durationMs)} of audio · took{" "}
648
+ {formatDuration(run.output.elapsedMs)} · {run.output.words} words
653
649
  </p>
654
650
  <pre className="whitespace-pre-wrap text-sm leading-relaxed">{run.output.transcript}</pre>
655
651
  </>
@@ -691,47 +687,26 @@ function LiveTranscript({ runId }: { runId: string }) {
691
687
  return (
692
688
  <div className="flex flex-col gap-2">
693
689
  <p className="text-xs opacity-60">
694
- {countWords(transcript)} words so far · through {clock(covered)}
690
+ {countWords(transcript)} words so far · through {formatDuration(covered)}
695
691
  </p>
696
692
  <pre className="whitespace-pre-wrap text-sm leading-relaxed opacity-80">{transcript}</pre>
697
693
  </div>
698
694
  );
699
695
  }
700
696
 
701
- /**
702
- * A duration a person can read.
703
- *
704
- * `${Math.round(ms / 1000)}s` was what this printed, and an hour-long recording came
705
- * out as `3746s` — which a reader asked whether they should parse as 37.46 seconds.
706
- * A raw second count stops being readable at about ninety of them, and the recordings
707
- * this desk is FOR are the ones past that.
708
- *
709
- * The hours component is omitted when it is zero rather than padded to `0:02:26`, so
710
- * a two-minute clip reads as `2:26` and only a long one grows a field.
711
- */
712
- function duration(ms: number): string {
713
- const total = Math.max(0, Math.round(ms / 1000));
714
- const seconds = String(total % 60).padStart(2, "0");
715
- const minutes = Math.floor(total / 60) % 60;
716
- const hours = Math.floor(total / 3600);
717
- return hours > 0
718
- ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
719
- : `${minutes}:${seconds}`;
720
- }
721
-
722
697
  /**
723
698
  * One line describing where a run has got to.
724
699
  *
725
- * A `Record` keyed by the status union rather than a switch, so a status added
726
- * to the SDK is a compile error here instead of falling through a `default:`
727
- * into whichever line was last.
700
+ * `WORKFLOW_STATUS_LABELS` is the SDK's neutral map a `Record` keyed by the
701
+ * status union rather than a switch, so a status added upstream is a compile
702
+ * error in one place every page inherits, and spreading a complete record cannot
703
+ * drop a key. Two of these keys are really this desk's: a page knows what its
704
+ * workflow does and the SDK does not.
728
705
  */
729
- const STATUS_LINE: Record<WorkflowRun["status"], string> = {
730
- pending: "Queued",
706
+ const STATUS_LINE = {
707
+ ...WORKFLOW_STATUS_LABELS,
731
708
  running: "Transcribing…",
732
709
  completed: "Transcript ready",
733
- failed: "Failed",
734
- cancelled: "Cancelled",
735
710
  };
736
711
 
737
712
  page({ name: "Transcription Desk", component: TranscriptionDesk });
@@ -8,13 +8,12 @@
8
8
  * the two ways to arrange that. The async API has no such cap. You submit a job, it
9
9
  * answers with an id in milliseconds, and the transcript is ready minutes later.
10
10
  *
11
- * So this flow is four steps and no arithmetic:
11
+ * So this flow is three steps and no arithmetic:
12
12
  *
13
13
  * ```text
14
14
  * uploadToProvider one step → the file, streamed, and the URL it answered
15
15
  * createJob one step → the transcript id
16
- * pollTranscript one step + a durable sleep, until it is done
17
- * readTranscript one step → the text
16
+ * pollTranscript one step + a durable sleep, until the text comes back
18
17
  * ```
19
18
  *
20
19
  * **It is here to be compared against the other two, and it usually wins.** No
@@ -24,6 +23,21 @@
24
23
  * inside: the latency is the provider's queue rather than your fan-out, and there is
25
24
  * nothing to report between "submitted" and "done" except the job's own status.
26
25
  *
26
+ * ## The endpoint is the SDK's; the STEPS are ours
27
+ *
28
+ * `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
29
+ * `@alexkroman1/aai/step` own the URL, the raw-key auth, the windowed streaming
30
+ * upload, the PLURAL `speech_models` field and the failure classification. This file
31
+ * used to spell all of that out, and so did `spoken-summary` — the same ~200 lines
32
+ * twice, reworded, identical in behaviour, and drifting apart at the edges.
33
+ *
34
+ * What stays here is what a dependency cannot decide: how many steps to cut the job
35
+ * into, and therefore what is journaled and what a retry repeats. That is also
36
+ * structural rather than stylistic — the Workflow DevKit's builder transforms
37
+ * exactly the files under this `workflows/` directory, so a `"use step"` shipped
38
+ * inside the SDK would be transformed by nothing and would run inline with no
39
+ * journal and no retry, silently.
40
+ *
27
41
  * ## The one thing that makes this a WORKFLOW rather than a request
28
42
  *
29
43
  * The wait. A job takes minutes, and nothing about an HTTP request survives minutes:
@@ -32,56 +46,23 @@
32
46
  * shape and its module doc carries the argument; this is the same pattern with the
33
47
  * poll bounded by attempts rather than by a deadline.
34
48
  *
35
- * ## The upload STREAMS out of our own store
49
+ * ## Three steps, not four, and the fourth was a wasted round trip
36
50
  *
37
- * `readUpload` hands back bytes, and a two-hour recording is not a value this process
38
- * can hold so the body sent to `/v2/upload` is an async iterable of windows, which
39
- * `stepFetch` accepts precisely for this. Nothing is buffered beyond one window.
40
- *
41
- * That is also why the step that does it is the step the DevKit retries: a streaming
42
- * body is consumed once, so a retry has to re-read the upload from the start, which
43
- * it does. One window of READ-AHEAD keeps the store and the socket busy at the same
44
- * time; `windows` carries the argument.
51
+ * This used to poll `GET /v2/transcript/:id` for a status and then fetch the
52
+ * identical URL a second time to read the text the poll already had in its hand.
53
+ * {@link pollTranscript} answers with the transcript, so a finished job costs one
54
+ * request rather than two and the value journaled by the last poll IS the result.
45
55
  */
46
56
 
47
- import { throwFatalStepError, toStepError } from "@alexkroman1/aai/step-errors";
48
- import { readUpload, report, stepFetch, uploadInfo } from "@alexkroman1/aai/utils";
57
+ import { report, TRANSCRIBE_API, uploadInfo } from "@alexkroman1/aai/step";
58
+ import {
59
+ stepTranscribePollClassified,
60
+ stepTranscribeSubmitClassified,
61
+ stepTranscribeUploadClassified,
62
+ } from "@alexkroman1/aai/step-errors";
63
+ import { countWords, formatBytes } from "@alexkroman1/aai/utils";
49
64
  import { sleep } from "workflow";
50
- import { apiKeyOrFatal } from "./sync-api.ts";
51
- import { countWords, startClock, type Transcript } from "./transcribe.ts";
52
-
53
- /** The async API's base. */
54
- const API = "https://api.assemblyai.com";
55
-
56
- /**
57
- * The models this desk asks for, best first.
58
- *
59
- * `speech_models`, PLURAL and an array. The singular `speech_model` is deprecated on
60
- * the async API and answers **400** for any current model name — which is how this
61
- * was found: the first live run of this flow failed on it, and the API said so in
62
- * exactly those words. Note the streaming API still uses the singular field, so the
63
- * two are not interchangeable and neither is "the" spelling.
64
- *
65
- * Omitting it entirely is also legal and routes to the default; naming it is what
66
- * pins the model so a default change does not silently move this template's output.
67
- */
68
- const MODELS = ["universal-3-5-pro"];
69
-
70
- /** How much of our stored upload one outbound window carries. */
71
- const UPLOAD_WINDOW_BYTES = 4 * 1024 * 1024;
72
-
73
- /** How long a single request may take. The upload is not one of these — see below. */
74
- const REQUEST_TIMEOUT_MS = 60_000;
75
-
76
- /**
77
- * How long the upload leg may take.
78
- *
79
- * Its own budget because it is the one request whose duration is a function of the
80
- * FILE rather than of the service: a gigabyte at 8 MB/s is over two minutes, and a
81
- * deadline sized for a JSON round trip would cancel exactly the uploads this flow
82
- * exists to handle.
83
- */
84
- const UPLOAD_TIMEOUT_MS = 30 * 60_000;
65
+ import { startClock, type Transcript } from "./transcribe.ts";
85
66
 
86
67
  /** How long between polls of a submitted job. */
87
68
  const POLL_INTERVAL = "10s";
@@ -89,9 +70,9 @@ const POLL_INTERVAL = "10s";
89
70
  /**
90
71
  * Polls before the run gives up on a job.
91
72
  *
92
- * At {@link POLL_INTERVAL} this is an hour, which is well past what the async API
93
- * takes for any recording it accepts. Bounded rather than endless because a job that
94
- * never leaves `queued` is a run that would otherwise be replayed forever.
73
+ * At {@link POLL_INTERVAL} this is an hour, well past what the async API takes for
74
+ * any recording it accepts. Bounded rather than endless because a job that never
75
+ * leaves `queued` is a run that would otherwise be replayed forever.
95
76
  */
96
77
  const MAX_POLLS = 360;
97
78
 
@@ -109,15 +90,15 @@ export async function transcribeBatchFlow(input: { recording: string }): Promise
109
90
  const job = await createJob(audioUrl);
110
91
 
111
92
  for (let poll = 0; poll < MAX_POLLS; poll += 1) {
112
- const status = await pollTranscript(job.id);
113
- if (status.done) return await readTranscript(input.recording, job.id, startedAt);
93
+ const progress = await pollTranscript(input.recording, job.id, startedAt);
94
+ if (progress.done) return progress.transcript;
114
95
  await sleep(POLL_INTERVAL);
115
96
  }
116
97
  // A plain throw: this is the BODY, where the fatal/retryable distinction has
117
98
  // nothing to apply to — see `stream.ts`'s `abandon` for the same reasoning.
118
99
  throw new Error(
119
100
  `Transcript ${job.id} was still unfinished after ${MAX_POLLS} polls. It is not lost — ` +
120
- `read it directly with GET ${API}/v2/transcript/${job.id}.`,
101
+ `read it directly with GET ${TRANSCRIBE_API}/v2/transcript/${job.id}.`,
121
102
  );
122
103
  }
123
104
 
@@ -134,28 +115,21 @@ export async function transcribeBatchFlow(input: { recording: string }): Promise
134
115
  * So the URL is journaled after all. The risk that made that look wrong is real but
135
116
  * far smaller: if it expires before the next step runs, the run fails and a fresh one
136
117
  * re-uploads — which is what would have happened anyway, once, instead of five times.
118
+ *
119
+ * The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
120
+ * `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns the
121
+ * SDK's `TranscribeError` into the DevKit's verdict: a missing key and a 400 stop, a
122
+ * 429 waits as long as the service asked. Every step here ends the same way for the
123
+ * same reason.
137
124
  */
138
125
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
139
126
  "use step";
140
127
 
141
- const apiKey = apiKeyOrFatal();
142
128
  const stored = await uploadInfo(uploadId);
143
- await report(`Uploading ${stored.name || uploadId} (${mb(stored.size)}) to the async API.`);
144
-
145
- const uploaded = await stepFetch(`${API}/v2/upload`, {
146
- method: "POST",
147
- headers: { Authorization: apiKey, "Content-Type": "application/octet-stream" },
148
- // An async iterable, not bytes: this file may be gigabytes, and nothing here holds
149
- // more than one window of it. See the module doc.
150
- body: windows(uploadId, stored.size),
151
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
152
- });
153
- if (!uploaded.ok) throw await failure(uploaded, "Upload");
154
- const { upload_url: audioUrl } = (await uploaded.json()) as { upload_url?: string };
155
- if (!audioUrl) {
156
- return throwFatalStepError(new Error("The async API accepted the upload but named no URL."));
157
- }
158
- return { audioUrl };
129
+ await report(
130
+ `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) to the async API.`,
131
+ );
132
+ return await stepTranscribeUploadClassified(uploadId);
159
133
  }
160
134
 
161
135
  /** Retries beyond the default 3: an upload is the one call here worth another attempt. */
@@ -165,126 +139,51 @@ uploadToProvider.maxRetries = 5;
165
139
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
166
140
  "use step";
167
141
 
168
- const created = await stepFetch(`${API}/v2/transcript`, {
169
- method: "POST",
170
- headers: { Authorization: apiKeyOrFatal(), "Content-Type": "application/json" },
171
- body: JSON.stringify({ audio_url: audioUrl, speech_models: MODELS }),
172
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
173
- });
174
- if (!created.ok) throw await failure(created, "Submit");
175
- const { id } = (await created.json()) as { id?: string };
176
- if (!id) return throwFatalStepError(new Error("The async API created no transcript id."));
177
-
178
- await report(`Submitted transcript ${id}.`);
179
- return { id };
142
+ const job = await stepTranscribeSubmitClassified(audioUrl);
143
+ await report(`Submitted — job ${job.id}.`);
144
+ return job;
180
145
  }
181
146
 
182
147
  /**
183
- * Ask once whether the job has finished.
148
+ * Ask once whether the job has finished, and read it when it has.
184
149
  *
185
150
  * `done` rather than the raw status, because the BODY branches on it and a body must
186
- * not be the place a provider's vocabulary is interpreted — a new status string would
187
- * otherwise be read as "not done yet" forever. A failed job is a terminal failure
188
- * here, not a `done: true` the caller has to re-check.
151
+ * not be where a provider's vocabulary is interpreted — a new status string would
152
+ * otherwise read as "not done yet" forever. A failed job is a terminal failure
153
+ * inside the SDK call, not a `done: true` the caller has to re-check.
189
154
  */
190
- export async function pollTranscript(id: string): Promise<{ done: boolean; status: string }> {
191
- "use step";
192
-
193
- const res = await stepFetch(`${API}/v2/transcript/${id}`, {
194
- headers: { Authorization: apiKeyOrFatal() },
195
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
196
- });
197
- if (!res.ok) throw await failure(res, `Transcript ${id}`);
198
- const body = (await res.json()) as { status?: string; error?: string };
199
- const status = body.status ?? "unknown";
200
- if (status === "error") {
201
- // The provider has decided; no number of polls changes it.
202
- return throwFatalStepError(
203
- new Error(
204
- `The async API could not transcribe that recording: ${body.error ?? "no reason given"}`,
205
- ),
206
- );
207
- }
208
- await report(`Transcript ${id} is ${status}.`);
209
- return { done: status === "completed", status };
210
- }
211
-
212
- /** Read the finished transcript, and report it the way both sync flows do. */
213
- export async function readTranscript(
155
+ export async function pollTranscript(
214
156
  uploadId: string,
215
157
  id: string,
216
158
  startedAt: number,
217
- ): Promise<Transcript> {
159
+ ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
218
160
  "use step";
219
161
 
220
- const res = await stepFetch(`${API}/v2/transcript/${id}`, {
221
- headers: { Authorization: apiKeyOrFatal() },
222
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
223
- });
224
- if (!res.ok) throw await failure(res, `Transcript ${id}`);
225
- const body = (await res.json()) as { text?: string; audio_duration?: number };
226
- const transcript = (body.text ?? "").trim();
227
- const stored = await uploadInfo(uploadId);
162
+ const progress = await stepTranscribePollClassified(id);
163
+ if (!progress.done) {
164
+ await report(`Transcript ${id} is ${progress.status}.`);
165
+ return { done: false };
166
+ }
228
167
 
168
+ const stored = await uploadInfo(uploadId);
169
+ const transcript = progress.transcript.text;
229
170
  return {
230
- source: stored.name || uploadId,
231
- // ONE, and it is not a fudge: the async API transcribed the recording in one
232
- // piece, which is the difference this flow is here to show. A reader comparing
233
- // the three sees 7 segments, 7 segments, and 1.
234
- segments: 1,
235
- // The provider's own measurement, in seconds the only one of the three flows
236
- // that does not have to derive this from byte offsets.
237
- durationMs: Math.round((body.audio_duration ?? 0) * 1000),
238
- // Wall clock, the same way both sync flows measure it — see `startClock`. For
239
- // this flow it is mostly the provider's queue, which is exactly the thing a
240
- // reader comparing the three wants to see.
241
- elapsedMs: Date.now() - startedAt,
242
- words: countWords(transcript),
243
- transcript,
171
+ done: true,
172
+ transcript: {
173
+ source: stored.name || uploadId,
174
+ // ONE, and it is not a fudge: the async API transcribed the recording in one
175
+ // piece, which is the difference this flow is here to show. A reader comparing
176
+ // the three sees 7 segments, 7 segments, and 1.
177
+ segments: 1,
178
+ // The provider's own measurement — the only one of the three flows that does
179
+ // not have to derive this from byte offsets.
180
+ durationMs: progress.transcript.durationMs,
181
+ // Wall clock, the same way both sync flows measure it — see `startClock`. For
182
+ // this flow it is mostly the provider's queue, which is exactly the thing a
183
+ // reader comparing the three wants to see.
184
+ elapsedMs: Date.now() - startedAt,
185
+ words: countWords(transcript),
186
+ transcript,
187
+ },
244
188
  };
245
189
  }
246
-
247
- /**
248
- * The stored upload as a sequence of windows, with the next one already in flight.
249
- *
250
- * A generator rather than one `readUpload`, because the whole point is that the file
251
- * is never held: each window is read, sent, and dropped. `readUpload` clamps to what
252
- * is stored, so the loop ends on the real end of the file even if `size` moved.
253
- *
254
- * **One window of READ-AHEAD**, which is the whole concurrency available here: the
255
- * consumer is a socket and the producer is the app's own store, and read-then-send
256
- * makes them strictly alternate — the store idles while bytes go out, and the socket
257
- * idles while the next window is fetched. Starting the next read before yielding the
258
- * current window overlaps them, so a gigabyte upload pays the larger of the two
259
- * rather than their sum. Exactly one, not a queue: a deeper buffer holds more of a
260
- * file this generator exists to avoid holding, and there is nothing to gain past
261
- * keeping both ends busy.
262
- */
263
- async function* windows(uploadId: string, size: number): AsyncGenerator<Uint8Array> {
264
- const read = (at: number): Promise<Uint8Array> =>
265
- readUpload(uploadId, { start: at, end: at + UPLOAD_WINDOW_BYTES }).then((slice) => slice.bytes);
266
- let at = 0;
267
- let next = at < size ? read(at) : undefined;
268
- while (next !== undefined) {
269
- const bytes = await next;
270
- if (bytes.length === 0) return;
271
- at += UPLOAD_WINDOW_BYTES;
272
- // Issued BEFORE the yield, so the store is fetching while the socket sends.
273
- next = at < size ? read(at) : undefined;
274
- yield bytes;
275
- }
276
- }
277
-
278
- /** A failed call, classified for the DevKit — see `sync-api.ts` for the three-way rule. */
279
- async function failure(res: Response, what: string): Promise<Error> {
280
- const body = (await res.json().catch(() => ({}))) as { error?: string };
281
- return toStepError(
282
- res,
283
- `${what} failed: HTTP ${res.status}${body.error ? ` — ${body.error}` : ""}`,
284
- );
285
- }
286
-
287
- /** A size a person can read, because the number that matters is the scale. */
288
- function mb(bytes: number): string {
289
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
290
- }