@alexkroman1/aai-cli 13.3.0 → 14.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 (134) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
  4. package/dist/_cli-common.d.ts +45 -6
  5. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  6. package/dist/_dev-env.d.ts +1 -1
  7. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
  8. package/dist/_dev-vite-config.d.ts +3 -3
  9. package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
  10. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  11. package/dist/_output.d.ts +10 -9
  12. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  13. package/dist/_resource-commands.d.ts +9 -9
  14. package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
  15. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  16. package/dist/_slug-api.d.ts +19 -0
  17. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  18. package/dist/_studio-commands.d.ts +13 -13
  19. package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
  20. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  21. package/dist/_typecheck-gate.d.ts +3 -1
  22. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  23. package/dist/_utils.d.ts +35 -0
  24. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  25. package/dist/_workflow-determinism.d.ts +3 -3
  26. package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
  27. package/dist/build.d.ts +5 -0
  28. package/dist/cli.d.ts +1 -2
  29. package/dist/cli.mjs +117 -135
  30. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
  31. package/dist/client-bundler.mjs +1 -1
  32. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  33. package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
  34. package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
  35. package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
  36. package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
  37. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  38. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  39. package/dist/project-config.mjs +1 -1
  40. package/dist/scaffold/CLAUDE.md +84 -56
  41. package/dist/scaffold/package.json +4 -4
  42. package/dist/scaffold/server.mjs +129 -25
  43. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  44. package/dist/secret.d.ts +8 -6
  45. package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
  46. package/dist/templates/call-audit/agent.test.ts +11 -6
  47. package/dist/templates/call-audit/agent.ts +2 -2
  48. package/dist/templates/call-audit/client.tsx +27 -42
  49. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  50. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  51. package/dist/templates/call-audit/workflows/media.ts +12 -37
  52. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  53. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  54. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  55. package/dist/templates/dispatch-center/client.tsx +53 -61
  56. package/dist/templates/dispatch-center/shared.ts +0 -8
  57. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  58. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  59. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  60. package/dist/templates/infocom-adventure/client.tsx +40 -36
  61. package/dist/templates/link-digest/agent.test.ts +3 -3
  62. package/dist/templates/link-digest/agent.ts +2 -2
  63. package/dist/templates/link-digest/client.tsx +27 -17
  64. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  65. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  66. package/dist/templates/night-owl/client.tsx +8 -2
  67. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  68. package/dist/templates/pizza-ordering/client.tsx +5 -4
  69. package/dist/templates/pizza-ordering/shared.ts +3 -8
  70. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  71. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  72. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  73. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  74. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  75. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  76. package/dist/templates/plan-and-execute/client.tsx +2 -2
  77. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  78. package/dist/templates/podcast-digest/client.tsx +16 -22
  79. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  80. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  81. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  82. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  83. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  84. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  85. package/dist/templates/redline/agent.eval.test.ts +5 -8
  86. package/dist/templates/redline/agent.test.ts +1 -1
  87. package/dist/templates/redline/client.tsx +36 -34
  88. package/dist/templates/redline/workflows/redline.ts +17 -17
  89. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  90. package/dist/templates/research-workflow/agent.test.ts +10 -7
  91. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  92. package/dist/templates/retail/agent.test.ts +17 -15
  93. package/dist/templates/retail/cancel.ts +7 -6
  94. package/dist/templates/retail/client.tsx +39 -58
  95. package/dist/templates/retail/payment.ts +4 -3
  96. package/dist/templates/retail/pending.test.ts +19 -19
  97. package/dist/templates/retail/store.test.ts +5 -5
  98. package/dist/templates/retail/swap.ts +8 -11
  99. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  100. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  101. package/dist/templates/solo-rpg/client.tsx +3 -3
  102. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  103. package/dist/templates/spoken-summary/agent.ts +3 -3
  104. package/dist/templates/spoken-summary/client.tsx +23 -30
  105. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  106. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  107. package/dist/templates/support-line/client.tsx +4 -3
  108. package/dist/templates/support-line/prompts.ts +0 -2
  109. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  110. package/dist/templates/transcription-workflow/agent.ts +2 -2
  111. package/dist/templates/transcription-workflow/client.tsx +15 -14
  112. package/dist/templates/transcription-workflow/recover.ts +1 -1
  113. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  114. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  115. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  116. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  117. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  118. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  119. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  120. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  121. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  122. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  123. package/dist/templates/travel-concierge/client.tsx +2 -2
  124. package/dist/templates/travel-concierge/shared.ts +10 -14
  125. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  126. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  127. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  128. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  129. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  130. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  131. package/dist/typecheck.mjs +1 -1
  132. package/dist/worker-bundler.mjs +1 -1
  133. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  134. package/package.json +4 -4
@@ -53,12 +53,12 @@
53
53
  * request rather than two and the value journaled by the last poll IS the result.
54
54
  */
55
55
 
56
- import type { WorkflowCtx } from "@alexkroman1/aai";
57
- import { report, TRANSCRIBE_API, uploadInfo } from "@alexkroman1/aai/step";
56
+ import type { WorkflowContext } from "@alexkroman1/aai";
57
+ import { stepReport, stepUploadInfo, TRANSCRIBE_API } from "@alexkroman1/aai/step";
58
58
  import {
59
- stepTranscribePollClassified,
60
- stepTranscribeSubmitClassified,
61
- stepTranscribeUploadClassified,
59
+ stepTranscribePollOrFail,
60
+ stepTranscribeSubmitOrFail,
61
+ stepTranscribeUploadOrFail,
62
62
  } from "@alexkroman1/aai/step-errors";
63
63
  import { countWords, formatBytes } from "@alexkroman1/aai/utils";
64
64
  import type { Transcript } from "./transcribe.ts";
@@ -78,7 +78,7 @@ const MAX_POLLS = 360;
78
78
  /** Transcribe a recording through the async API. */
79
79
  export async function transcribeBatchFlow(
80
80
  input: { recording: string },
81
- ctx: WorkflowCtx,
81
+ ctx: WorkflowContext,
82
82
  ): Promise<Transcript> {
83
83
  // Both at once: the clock does not depend on the upload, and issuing them
84
84
  // together costs one round trip instead of two before a byte moves. Their issue
@@ -122,24 +122,24 @@ export async function transcribeBatchFlow(
122
122
  * far smaller: if it expires before the next step runs, the run fails and a fresh one
123
123
  * re-uploads — which is what would have happened anyway, once, instead of five times.
124
124
  *
125
- * The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
125
+ * The `OrFail` callers on `@alexkroman1/aai/step-errors` are the SDK's own
126
126
  * `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns the
127
127
  * SDK's `TranscribeError` into the DevKit's verdict: a missing key and a 400 stop, a
128
128
  * 429 waits as long as the service asked. Every step here ends the same way for the
129
129
  * same reason.
130
130
  */
131
131
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
132
- const stored = await uploadInfo(uploadId);
133
- await report(
132
+ const stored = await stepUploadInfo(uploadId);
133
+ await stepReport(
134
134
  `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) to the async API.`,
135
135
  );
136
- return await stepTranscribeUploadClassified(uploadId);
136
+ return await stepTranscribeUploadOrFail(uploadId);
137
137
  }
138
138
 
139
139
  /** Create the transcription job, and answer with the id that outlives this run. */
140
140
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
141
- const job = await stepTranscribeSubmitClassified(audioUrl);
142
- await report(`Submitted — job ${job.id}.`);
141
+ const job = await stepTranscribeSubmitOrFail(audioUrl);
142
+ await stepReport(`Submitted — job ${job.id}.`);
143
143
  return job;
144
144
  }
145
145
 
@@ -156,13 +156,13 @@ export async function pollTranscript(
156
156
  id: string,
157
157
  startedAt: number,
158
158
  ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
159
- const progress = await stepTranscribePollClassified(id);
159
+ const progress = await stepTranscribePollOrFail(id);
160
160
  if (!progress.done) {
161
- await report(`Transcript ${id} is ${progress.status}.`);
161
+ await stepReport(`Transcript ${id} is ${progress.status}.`);
162
162
  return { done: false };
163
163
  }
164
164
 
165
- const stored = await uploadInfo(uploadId);
165
+ const stored = await stepUploadInfo(uploadId);
166
166
  const transcript = progress.transcript.text;
167
167
  return {
168
168
  done: true,
@@ -152,7 +152,7 @@ export function downsampleSegment(
152
152
  const bind = sampleReaderFor(from);
153
153
  const frame = blockAlign(from);
154
154
  const inFrames = Math.floor(bytes.length / frame);
155
- // A CALLER ERROR, and stated as one. `readUpload` clamps a window to what is
155
+ // A CALLER ERROR, and stated as one. `stepReadUpload` clamps a window to what is
156
156
  // stored, so the streaming flow can hand a short read down here; with no whole
157
157
  // frame the averaging window below is zero wide, `total / 0` is `NaN`, and
158
158
  // `setInt16` writes that as a 0 — a two-byte WAV of silence, transcribed and
@@ -70,8 +70,8 @@
70
70
  */
71
71
 
72
72
  import { basename, extname, join } from "node:path";
73
- import { probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
74
- import { readUpload, report, requireCompleteUpload } from "@alexkroman1/aai/step";
73
+ import { ffmpegBaseArgs, probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
74
+ import { stepReadUpload, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
75
75
  import { throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
76
76
  import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
77
77
  import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
@@ -122,16 +122,16 @@ export type NormalizedRecording = {
122
122
  * file that already exists instead of paying for a second one.
123
123
  */
124
124
  export async function normalizeRecording(uploadId: string): Promise<NormalizedRecording> {
125
- // `requireCompleteUpload`, not `uploadInfo`: `size` is the readable PREFIX, and
125
+ // `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, and
126
126
  // every judgement below — cuttable, heavier-per-second, the byte count copied to
127
127
  // disk — is about the WHOLE file.
128
- const stored = await requireCompleteUpload(uploadId);
129
- const head = await readUpload(uploadId, { end: HEADER_PROBE_BYTES });
128
+ const stored = await stepRequireCompleteUpload(uploadId);
129
+ const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
130
130
 
131
131
  if (cuttable(head.bytes, stored.size) && !heavierThanNormalized(head.bytes, stored.size)) {
132
132
  // No subprocess, no copy, no second upload. The overwhelmingly common case
133
133
  // for a desk whose form says WAV, and the reason the check is a 64 KB read.
134
- await report(`${stored.name || uploadId} is already linear-PCM WAV — cutting it as it is.`);
134
+ await stepReport(`${stored.name || uploadId} is already linear-PCM WAV — cutting it as it is.`);
135
135
  return { recording: uploadId, converted: false };
136
136
  }
137
137
 
@@ -144,7 +144,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
144
144
  // WAV that is fine but too heavy to cut at this rate. Reporting "not a WAV we
145
145
  // can cut" for the second one is a line that contradicts the file they
146
146
  // uploaded.
147
- await report(
147
+ await stepReport(
148
148
  `Converting ${stored.name || uploadId} (${formatBytes(stored.size)}) — ` +
149
149
  (cuttable(head.bytes, stored.size)
150
150
  ? `heavier per second than ${NORMALIZED_SAMPLE_RATE / 1000} kHz mono.`
@@ -164,7 +164,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
164
164
  // `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
165
165
  // completeness myself", which is what a body polling a still-arriving
166
166
  // upload needs and is the opposite of what happened above: this step has
167
- // already called `requireCompleteUpload`, so the file IS whole and the
167
+ // already called `stepRequireCompleteUpload`, so the file IS whole and the
168
168
  // windows may land in any order. Omitting it lets `readUploadToFile`
169
169
  // establish that for itself and fan out. The cost is one metadata round
170
170
  // trip, against the dozens of window reads it overlaps.
@@ -177,22 +177,18 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
177
177
  const info = await probeMedia(source, { timeoutMs: CONVERT_TIMEOUT_MS }).catch(
178
178
  throwFfmpegStepError,
179
179
  );
180
- await report(
180
+ await stepReport(
181
181
  `It is ${describeSource(info.audio?.codec, info.durationSec)} — re-encoding to ` +
182
182
  `${NORMALIZED_SAMPLE_RATE / 1000} kHz mono WAV.`,
183
183
  );
184
184
 
185
185
  await runFfmpeg(
186
186
  [
187
- // The argv is the caller's, verbatim — `runFfmpeg` adds nothing. So the
188
- // standing flags are here: quiet, non-interactive, overwrite. `-nostdin`
189
- // matters most in a guest, where there is no terminal and an ffmpeg that
190
- // decides to read stdin is a process that never exits.
191
- "-hide_banner",
192
- "-loglevel",
193
- "error",
194
- "-nostdin",
195
- "-y",
187
+ // The argv is the caller's, verbatim — `runFfmpeg` adds nothing so
188
+ // the standing flags are the caller's too. This copy used to omit
189
+ // `-nostats`, which is the one that keeps ffmpeg's progress spam from
190
+ // evicting the error out of the captured stderr tail.
191
+ ...ffmpegBaseArgs(),
196
192
  "-i",
197
193
  source,
198
194
  ...wavEncodeArgs({
@@ -212,7 +208,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
212
208
  type: "audio/wav",
213
209
  });
214
210
 
215
- await report(
211
+ await stepReport(
216
212
  `Converted to ${formatBytes(written.size)} of WAV (from ${formatBytes(stored.size)}).`,
217
213
  );
218
214
  return { recording: written.id, converted: true };
@@ -22,7 +22,7 @@
22
22
  * first byte with `complete: false` and its `size` grows as bytes land.
23
23
  *
24
24
  * The reader needed almost nothing for this, which is why this flow is so close to
25
- * the other one: `readUpload` already clamped its window to what is stored (so a
25
+ * the other one: `stepReadUpload` already clamped its window to what is stored (so a
26
26
  * plan computed from a header could end one byte past the file), and that clamp is
27
27
  * exactly "read what has arrived". So `transcribeSegment` below is `transcribe.ts`'s
28
28
  * OWN step, unchanged, called on windows this body has checked are present.
@@ -50,7 +50,7 @@
50
50
  * 0.9 MB/s: `size` was 0 at every poll for 45 seconds and then the whole file, so
51
51
  * the run planned nothing, transcribed nothing, and did its entire fan-out after
52
52
  * the upload — the classic flow, with extra steps. `segmentStored` reads `ranges`
53
- * instead, and `readUpload` clamps to the run a read starts in rather than to the
53
+ * instead, and `stepReadUpload` clamps to the run a read starts in rather than to the
54
54
  * prefix, so a window that has landed is a window this flow can work on.
55
55
  * - **The stall test on the prefix would then FAIL a healthy upload.** A parts
56
56
  * upload moving at full speed reports the same prefix at every poll, which is
@@ -167,13 +167,13 @@
167
167
  * what keeps that order a pure function of journaled values.
168
168
  */
169
169
 
170
- import type { WorkflowCtx } from "@alexkroman1/aai";
170
+ import type { WorkflowContext } from "@alexkroman1/aai";
171
171
  import {
172
172
  mapConcurrent,
173
- readUpload,
174
- report,
173
+ stepReadUpload,
174
+ stepReport,
175
+ stepUploadInfo,
175
176
  type UploadRange,
176
- uploadInfo,
177
177
  } from "@alexkroman1/aai/step";
178
178
  import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
179
179
  import { formatDuration, omitUndefined, plural } from "@alexkroman1/aai/utils";
@@ -308,7 +308,7 @@ export type StreamPlan = {
308
308
  * an upload id exactly as in the classic flow; what differs is that the client chose
309
309
  * it and the bytes are still on their way.
310
310
  */
311
- export async function transcribeStreamFlow(input: { recording: string }, ctx: WorkflowCtx) {
311
+ export async function transcribeStreamFlow(input: { recording: string }, ctx: WorkflowContext) {
312
312
  // `ctx.now()`, not a step: the engine journals the read under its own key, so
313
313
  // every walk of this line sees the instant the first one did.
314
314
  const startedAt = await ctx.now();
@@ -364,7 +364,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
364
364
  // A segment is READY when its whole window is stored — except once the upload
365
365
  // is complete, where `at.size` is the true total and the plan came from the
366
366
  // header's DECLARED length: a recording that came up short leaves a final
367
- // segment ending past the file, and `readUpload` clamping is what makes that
367
+ // segment ending past the file, and `stepReadUpload` clamping is what makes that
368
368
  // the right answer rather than an error.
369
369
  const ready = plan.segments.filter(
370
370
  (segment) =>
@@ -443,7 +443,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
443
443
  * from.
444
444
  */
445
445
  export async function probeUpload(id: string): Promise<UploadProgressView> {
446
- const info = await uploadInfo(id);
446
+ const info = await stepUploadInfo(id);
447
447
  return {
448
448
  size: info.size,
449
449
  complete: info.complete,
@@ -609,7 +609,7 @@ export function storedBytes(size: number, ranges: readonly UploadRange[] | undef
609
609
  * only the prefix and the run has nothing to do until the upload is over, which
610
610
  * is the entire wait this flow exists to remove.
611
611
  *
612
- * A window has to be covered WHOLE by one run: `readUpload` clamps to the run a
612
+ * A window has to be covered WHOLE by one run: `stepReadUpload` clamps to the run a
613
613
  * read starts in, so a segment straddling a hole would come back short and be
614
614
  * transcribed as a fragment. `rangesOf` merges adjacent windows, so a run really
615
615
  * is a contiguous stretch and one containment test is the whole check.
@@ -636,7 +636,7 @@ export function segmentStored(segment: Segment, at: UploadProgressView): boolean
636
636
  * which is what the classic flow is for.
637
637
  */
638
638
  export async function planStreamed(id: string): Promise<StreamPlan> {
639
- const head = await readUpload(id, { end: HEADER_PROBE_BYTES });
639
+ const head = await stepReadUpload(id, { end: HEADER_PROBE_BYTES });
640
640
  const format = fatalOnUnsupported(() => parseWav(head.bytes, Number.POSITIVE_INFINITY));
641
641
  if (!Number.isFinite(format.dataEnd)) {
642
642
  return throwFatalStepError(
@@ -647,7 +647,7 @@ export async function planStreamed(id: string): Promise<StreamPlan> {
647
647
  );
648
648
  }
649
649
  const segments = fatalOnUnsupported(() => planSegments(format));
650
- await report(
650
+ await stepReport(
651
651
  `Planned ${formatDuration(segments.at(-1)?.endMs ?? 0)} of audio as ` +
652
652
  `${segments.length} ${plural(segments.length, "segment")} while it uploads.`,
653
653
  );
@@ -18,7 +18,7 @@
18
18
  * the same shape). It is called FROM steps, so it inherits their environment.
19
19
  */
20
20
 
21
- import { stepTranscribeSyncClassified } from "@alexkroman1/aai/step-errors";
21
+ import { stepTranscribeSyncOrFail } from "@alexkroman1/aai/step-errors";
22
22
 
23
23
  /**
24
24
  * Time one transcription, so the progress log carries LATENCY.
@@ -57,7 +57,7 @@ export function elapsed(ms: number): string {
57
57
  * concatenated straight into the request body rather than into an intermediate
58
58
  * buffer that doubles the segment's footprint.
59
59
  *
60
- * `stepTranscribeSyncClassified` — the SDK's own `stepTranscribeSync` plus
60
+ * `stepTranscribeSyncOrFail` — the SDK's own `stepTranscribeSync` plus
61
61
  * `throwStepError`, and nothing else — is the whole of what this adds to the SDK
62
62
  * call, and it is where the three-way call is made: a `FatalError` stops the DevKit retrying
63
63
  * something that will answer the same way, a bare `RetryableError` retries in ONE
@@ -75,6 +75,6 @@ export async function transcribeWav(
75
75
  filename: string,
76
76
  label: string,
77
77
  ): Promise<string> {
78
- const { text } = await stepTranscribeSyncClassified(bytes, { filename, label });
78
+ const { text } = await stepTranscribeSyncOrFail(bytes, { filename, label });
79
79
  return text;
80
80
  }
@@ -48,7 +48,7 @@
48
48
  * - **The audio is addressed by BYTE RANGE, never carried.** A workflow's input
49
49
  * is journaled and replayed on every resume, so the recording lives in the
50
50
  * app's own upload store and the run carries only its id; each step reads
51
- * exactly its own window with `readUpload`. Sixty steps therefore move the
51
+ * exactly its own window with `stepReadUpload`. Sixty steps therefore move the
52
52
  * recording once between them, not sixty times.
53
53
  * - **The fan-out is bounded by `mapConcurrent`, and the bound is not a detail.**
54
54
  * The DevKit correlates a journal entry to a step call by the ORDER the call
@@ -57,20 +57,20 @@
57
57
  * calls settle. Its module doc carries the argument; what matters here is that
58
58
  * there is no barrier, so a slow segment costs only itself.
59
59
  * - **The transcript STREAMS as it is produced.** Each segment is emitted the
60
- * moment it lands (`emit(TRANSCRIPT_STREAM, …)`), so the page renders the
60
+ * moment it lands (`stepEmit(TRANSCRIPT_STREAM, …)`), so the page renders the
61
61
  * answer growing rather than a status line and then everything at once. That is
62
62
  * the difference a fan-out can make to a reader and a run output cannot: an
63
63
  * `output` exists only when the last segment does.
64
64
  */
65
65
 
66
- import type { WorkflowCtx } from "@alexkroman1/aai";
66
+ import type { WorkflowContext } from "@alexkroman1/aai";
67
67
  import {
68
- emit,
69
68
  mapConcurrent,
70
- readUpload,
71
- report,
72
- requireCompleteUpload,
73
- uploadInfo,
69
+ stepEmit,
70
+ stepReadUpload,
71
+ stepReport,
72
+ stepRequireCompleteUpload,
73
+ stepUploadInfo,
74
74
  wavHeader,
75
75
  } from "@alexkroman1/aai/step";
76
76
  import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
@@ -248,7 +248,7 @@ export type SegmentTranscript = {
248
248
  * The input is what `POST /workflows/runs` carries — see `agent.ts` for the
249
249
  * schema it is validated against before a run exists.
250
250
  */
251
- export async function transcribeFlow(input: { recording: string }, ctx: WorkflowCtx) {
251
+ export async function transcribeFlow(input: { recording: string }, ctx: WorkflowContext) {
252
252
  // Both at once: neither needs the other, and issued together they are one
253
253
  // round trip instead of two before any audio is read. The ORDER is still a
254
254
  // pure function of this line — the two calls go out synchronously, left to
@@ -313,13 +313,13 @@ export async function splitRecording(uploadId: string): Promise<{
313
313
  // half-arrived recording this planned a fan-out over the first half and the run
314
314
  // returned a transcript of it, reporting success. `stream.ts` is the flow for a
315
315
  // recording that is still landing; this one wants all of it.
316
- const stored = await requireCompleteUpload(uploadId);
317
- const head = await readUpload(uploadId, { end: HEADER_PROBE_BYTES });
316
+ const stored = await stepRequireCompleteUpload(uploadId);
317
+ const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
318
318
  const format = fatalOnUnsupported(() => parseWav(head.bytes, stored.size));
319
319
  const segments = fatalOnUnsupported(() => planSegments(format));
320
320
  const durationMs = segments.at(-1)?.endMs ?? 0;
321
321
 
322
- await report(
322
+ await stepReport(
323
323
  `Split ${formatDuration(durationMs)} of audio into ${segments.length} ${plural(segments.length, "segment")}.`,
324
324
  );
325
325
  return { format, segments, durationMs };
@@ -345,12 +345,14 @@ export async function transcribeSegment(
345
345
  // calls together, so their lines interleave by completion — the page renders a
346
346
  // log, not a sequence, and `segment.index` is what puts the TRANSCRIPT back in
347
347
  // order.
348
- await report(`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`);
348
+ await stepReport(
349
+ `Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
350
+ );
349
351
 
350
352
  // `[start, end)`, the same half-open pair `planSegments` produced — the store
351
353
  // owns the conversion to HTTP's inclusive range, so there is no `- 1` here to
352
354
  // get wrong.
353
- const audio = await readUpload(uploadId, { start: segment.start, end: segment.end });
355
+ const audio = await stepReadUpload(uploadId, { start: segment.start, end: segment.end });
354
356
 
355
357
  // The audio and nothing else. A `config` part carrying `language_code` used
356
358
  // to ride along, and it is gone with the picker that fed it: the model detects
@@ -401,15 +403,15 @@ export async function transcribeSegment(
401
403
  );
402
404
  // The LATENCY, which is what says whether the concurrency bound or the endpoint
403
405
  // is the thing limiting the run — see `timed`'s doc.
404
- await report(
406
+ await stepReport(
405
407
  `Transcribed ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)} in ${elapsed(ms)}.`,
406
408
  );
407
409
  // And the WORDS, into their own stream, which is what makes this run's answer
408
410
  // streamable rather than only its narration: the page stitches whatever has
409
411
  // arrived and renders the transcript growing, minutes before `output` exists.
410
- // Its own namespace because `report`'s stream carries sentences a page prints
411
- // verbatim — see `emit`'s doc.
412
- await emit(TRANSCRIPT_STREAM, {
412
+ // Its own namespace because `stepReport`'s stream carries sentences a page prints
413
+ // verbatim — see `stepEmit`'s doc.
414
+ await stepEmit(TRANSCRIPT_STREAM, {
413
415
  index: segment.index,
414
416
  startMs: segment.startMs,
415
417
  endMs: segment.endMs,
@@ -422,7 +424,7 @@ export async function transcribeSegment(
422
424
  * Stitch the segments into one transcript.
423
425
  *
424
426
  * A step rather than a pure call in the body, and the reason is the narration:
425
- * the body replays from the top on every resume, so a `report()` written there
427
+ * the body replays from the top on every resume, so a `stepReport()` written there
426
428
  * is re-emitted on each one. Journaling the finished transcript also means a
427
429
  * caller re-reading a completed run gets the same bytes rather than a value
428
430
  * recomputed from parts.
@@ -433,7 +435,7 @@ export async function mergeTranscript(
433
435
  parts: readonly SegmentTranscript[],
434
436
  startedAt: number,
435
437
  ): Promise<Transcript> {
436
- await report(`Stitching ${parts.length} ${plural(parts.length, "segment")} together.`);
438
+ await stepReport(`Stitching ${parts.length} ${plural(parts.length, "segment")} together.`);
437
439
 
438
440
  // `mapConcurrent` resolves in ITEM order however the calls settled, so this is
439
441
  // already ordered — sorted anyway, because the merge is where an ordering
@@ -443,7 +445,7 @@ export async function mergeTranscript(
443
445
 
444
446
  // The FILENAME, not the id: the page prints this, and `upl_9f3…` tells a
445
447
  // reader nothing about which recording they are looking at.
446
- const source = (await uploadInfo(uploadId)).name || uploadId;
448
+ const source = (await stepUploadInfo(uploadId)).name || uploadId;
447
449
  return {
448
450
  source,
449
451
  segments: parts.length,
@@ -457,19 +459,6 @@ export async function mergeTranscript(
457
459
  };
458
460
  }
459
461
 
460
- // Re-exported rather than re-declared: `stream.ts` and `batch.ts` already import
461
- // these from this module, and the split that let the PAGE stitch a partial
462
- // transcript should not ripple through every flow. `clock` and `countWords` used
463
- // to be in this list and are `formatDuration`/`countWords` on
464
- // `@alexkroman1/aai/utils` now — a run narrates itself and the page renders the
465
- // same run, so those two were a private copy of a formatter the SDK ships.
466
- export {
467
- stitchChunks,
468
- stitchTranscript,
469
- TRANSCRIPT_STREAM,
470
- type TranscriptChunk,
471
- } from "./stitch.ts";
472
-
473
462
  // ---- I/O helpers ------------------------------------------------------------
474
463
 
475
464
  /**
@@ -30,11 +30,11 @@
30
30
  */
31
31
  import agentDef from "virtual:aai/agent";
32
32
  import {
33
- callsIn,
34
33
  describeTurn,
35
34
  type EvalSession,
36
35
  type EvalToolCall,
37
36
  lastStateIn,
37
+ toolCallsInTurns,
38
38
  toolNames,
39
39
  turnCalling,
40
40
  } from "@alexkroman1/aai-runtime/eval";
@@ -116,7 +116,7 @@ describeEval(agentDef, (test) => {
116
116
  // every turn's tool list AND tells the two findings apart: no
117
117
  // `update_ticket` at all, or calls that were all refused by the desk gate.
118
118
  const staging = turnCalling(turns, "update_ticket", stagedSomething);
119
- const attempts = callsIn(turns).filter((call) => call.name === "update_ticket");
119
+ const attempts = toolCallsInTurns(turns).filter((call) => call.name === "update_ticket");
120
120
  // The staging call itself, by INDEX, because what follows it in the same
121
121
  // turn is the subject of the assertion below.
122
122
  const stagedAt = staging.toolCalls.findIndex(
@@ -1,7 +1,12 @@
1
1
  /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
2
  import agentDef from "virtual:aai/agent";
3
3
  import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, ok, okPosition, toolRunner } from "@alexkroman1/aai/testing";
4
+ import {
5
+ createToolContext,
6
+ expectDialogOk,
7
+ expectToolOk,
8
+ toolRunner,
9
+ } from "@alexkroman1/aai/testing";
5
10
  import { describe, expect, test } from "vitest";
6
11
 
7
12
  import {
@@ -196,11 +201,11 @@ describe("sensitive tools stage rather than act", () => {
196
201
  expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
197
202
 
198
203
  // A gated tool answers the flow's POSITION wrapped around the body's own
199
- // return value, so the applied sentence is under `result`. `okPosition`
204
+ // return value, so the applied sentence is under `result`. `expectDialogOk`
200
205
  // keeps the position and THROWS on a refusal, quoting it — where the cast
201
206
  // it replaces read `undefined` off the failure and died three assertions
202
207
  // later on a property of undefined.
203
- const confirmed = okPosition<{ applied: string; reference: string }>(
208
+ const confirmed = expectDialogOk<{ applied: string; reference: string }>(
204
209
  await run("confirm_action", ctx),
205
210
  );
206
211
  expect(confirmed.result.applied).toContain("Harborview Suites");
@@ -225,7 +230,7 @@ describe("sensitive tools stage rather than act", () => {
225
230
  const ctx = makeCtx();
226
231
  await atDesk("flight", ctx);
227
232
  await run("update_ticket", { flightId: "LX54" }, ctx);
228
- const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
233
+ const dropped = expectDialogOk<{ discarded: string }>(await run("cancel_action", ctx));
229
234
  expect(dropped.result.discarded).toContain("LX54");
230
235
  expect(dropped.state).toBe("browsing");
231
236
 
@@ -291,9 +296,9 @@ describe("sensitive tools stage rather than act", () => {
291
296
  // A refused SECOND staging must not have moved the gate either — it was
292
297
  // already `awaitingConfirmation` and the refusal changed nothing.
293
298
  expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
294
- // `ok` is `okPosition` with `.result` taken off: this assertion is about
299
+ // `expectToolOk` is `expectDialogOk` with `.result` taken off: this assertion is about
295
300
  // what the apply DID, not about where the gate landed.
296
- const applied = ok<{ applied: string }>(await run("confirm_action", ctx));
301
+ const applied = expectToolOk<{ applied: string }>(await run("confirm_action", ctx));
297
302
  expect(applied.applied).toContain("LX52");
298
303
  expect(stateOf(ctx).bookings).toEqual([]);
299
304
 
@@ -368,7 +373,10 @@ describe("search tools", () => {
368
373
  const all = (await run("search_hotels", { city: "Boston" }, ctx)) as {
369
374
  hotels: { perNight: string }[];
370
375
  };
371
- expect(all.hotels.map((h) => h.perNight)).toEqual(["$180", "$265", "$340"]);
376
+ // Always to the cent: `formatMoney` is one shape at every desk, where
377
+ // this template's own `toLocaleString` copy dropped `.00` on a round
378
+ // number and kept it on a price with change.
379
+ expect(all.hotels.map((h) => h.perNight)).toEqual(["$180.00", "$265.00", "$340.00"]);
372
380
 
373
381
  const cheap = (await run("search_hotels", { city: "Boston", maxPerNight: 200 }, ctx)) as {
374
382
  hotels: { name: string }[];
@@ -1,5 +1,5 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
2
+ import { AutoScroll, mountClient, useAgentState } from "@alexkroman1/aai-ui";
3
3
  import type { TripView } from "./shared.ts";
4
4
  import { SPECIALIST_IDS, SPECIALISTS, tripProjection } from "./shared.ts";
5
5
 
@@ -118,7 +118,7 @@ function ItinerarySidebar() {
118
118
  );
119
119
  }
120
120
 
121
- client({
121
+ mountClient({
122
122
  name: "Swiss Air Concierge",
123
123
  sidebar: ItinerarySidebar,
124
124
  theme: {
@@ -47,6 +47,7 @@ import {
47
47
  type ToolContext,
48
48
  type ToolFailure,
49
49
  } from "@alexkroman1/aai";
50
+ import { formatMoney, plural } from "@alexkroman1/aai/utils";
50
51
 
51
52
  // ─── The booking world ───────────────────────────────────────────────────────
52
53
  // Their notebook downloads a sqlite database of a real airline's schedule and
@@ -180,11 +181,6 @@ export const EXCURSIONS: Excursion[] = [
180
181
  { id: "E4", name: "Lake Zurich cruise", city: "Zurich", kind: "boat", price: 52 },
181
182
  ];
182
183
 
183
- /** `$1,234` — one money format, so a spoken price and a rendered one agree. */
184
- export function formatPrice(amount: number): string {
185
- return `$${amount.toLocaleString("en-US")}`;
186
- }
187
-
188
184
  // ─── The dialog stack ────────────────────────────────────────────────────────
189
185
 
190
186
  export const SPECIALIST_IDS = ["flight", "hotel", "car_rental", "excursion"] as const;
@@ -504,24 +500,24 @@ export function describeAction(action: DeepReadonly<PendingAction>): string | To
504
500
  case "update_ticket": {
505
501
  const flight = FLIGHTS.find((f) => f.id === action.flightId);
506
502
  if (!flight) return { error: `No flight ${action.flightId} in the schedule.` };
507
- return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatPrice(flight.fare)}`;
503
+ return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatMoney(flight.fare)}`;
508
504
  }
509
505
  case "cancel_ticket":
510
506
  return "cancel your ticket entirely";
511
507
  case "book_hotel": {
512
508
  const hotel = HOTELS.find((h) => h.id === action.hotelId);
513
509
  if (!hotel) return { error: `No hotel ${action.hotelId}.` };
514
- return `book ${hotel.name} in ${hotel.area} for ${action.nights} night${action.nights === 1 ? "" : "s"}, ${formatPrice(hotel.pricePerNight * action.nights)} total`;
510
+ return `book ${hotel.name} in ${hotel.area} for ${action.nights} ${plural(action.nights, "night")}, ${formatMoney(hotel.pricePerNight * action.nights)} total`;
515
511
  }
516
512
  case "book_car": {
517
513
  const car = CAR_RENTALS.find((c) => c.id === action.carId);
518
514
  if (!car) return { error: `No car ${action.carId}.` };
519
- return `reserve the ${car.tier} from ${car.vendor} for ${action.days} day${action.days === 1 ? "" : "s"}, ${formatPrice(car.pricePerDay * action.days)} total`;
515
+ return `reserve the ${car.tier} from ${car.vendor} for ${action.days} ${plural(action.days, "day")}, ${formatMoney(car.pricePerDay * action.days)} total`;
520
516
  }
521
517
  case "book_excursion": {
522
518
  const excursion = EXCURSIONS.find((e) => e.id === action.excursionId);
523
519
  if (!excursion) return { error: `No excursion ${action.excursionId}.` };
524
- return `book ${excursion.name} at ${formatPrice(excursion.price)}`;
520
+ return `book ${excursion.name} at ${formatMoney(excursion.price)}`;
525
521
  }
526
522
  // Unreachable while `PendingAction` is exhausted above — and the arm a new
527
523
  // member of the union lands in until it has one of its own, which is a
@@ -627,11 +623,11 @@ export function applyPending(
627
623
  state.bookings.push({
628
624
  kind: "hotel",
629
625
  reference,
630
- summary: `${hotel.name} (${hotel.area}), ${action.nights} night${action.nights === 1 ? "" : "s"}`,
626
+ summary: `${hotel.name} (${hotel.area}), ${action.nights} ${plural(action.nights, "night")}`,
631
627
  price,
632
628
  });
633
629
  note(state, `Hotel booked: ${hotel.name} — ${reference}`);
634
- return { applied: `${hotel.name} booked, ${formatPrice(price)}.`, reference };
630
+ return { applied: `${hotel.name} booked, ${formatMoney(price)}.`, reference };
635
631
  }
636
632
  case "book_car": {
637
633
  const car = CAR_RENTALS.find((c) => c.id === action.carId);
@@ -640,12 +636,12 @@ export function applyPending(
640
636
  state.bookings.push({
641
637
  kind: "car",
642
638
  reference,
643
- summary: `${car.vendor} ${car.tier}, ${action.days} day${action.days === 1 ? "" : "s"}`,
639
+ summary: `${car.vendor} ${car.tier}, ${action.days} ${plural(action.days, "day")}`,
644
640
  price,
645
641
  });
646
642
  note(state, `Car reserved: ${car.vendor} ${car.tier} — ${reference}`);
647
643
  return {
648
- applied: `${car.tier} from ${car.vendor} reserved, ${formatPrice(price)}.`,
644
+ applied: `${car.tier} from ${car.vendor} reserved, ${formatMoney(price)}.`,
649
645
  reference,
650
646
  };
651
647
  }
@@ -659,7 +655,7 @@ export function applyPending(
659
655
  price: excursion.price,
660
656
  });
661
657
  note(state, `Excursion booked: ${excursion.name} — ${reference}`);
662
- return { applied: `${excursion.name} booked, ${formatPrice(excursion.price)}.`, reference };
658
+ return { applied: `${excursion.name} booked, ${formatMoney(excursion.price)}.`, reference };
663
659
  }
664
660
  // Same as `describeAction`: unreachable today, and a refusal rather than a
665
661
  // silent no-op for whatever the union grows next.
@@ -1,4 +1,5 @@
1
- import { FLIGHTS, formatPrice, tripSlot } from "../shared.ts";
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
+ import { FLIGHTS, tripSlot } from "../shared.ts";
2
3
 
3
4
  /**
4
5
  * Their `fetch_user_flight_information`, which the notebook runs ONCE and
@@ -22,13 +23,13 @@ export default tripSlot.tool({
22
23
  route: flight.route,
23
24
  departs: flight.departs,
24
25
  arrives: flight.arrives,
25
- fare: formatPrice(flight.fare),
26
+ fare: formatMoney(flight.fare),
26
27
  }
27
28
  : null,
28
29
  bookings: trip.bookings.map((b) => ({
29
30
  reference: b.reference,
30
31
  what: b.summary,
31
- price: formatPrice(b.price),
32
+ price: formatMoney(b.price),
32
33
  })),
33
34
  };
34
35
  },