@alexkroman1/aai-cli 13.3.0 → 15.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 (158) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  4. package/dist/_artifacts.d.ts +16 -0
  5. package/dist/_build-target.d.ts +172 -0
  6. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
  7. package/dist/_cli-common.d.ts +45 -6
  8. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  9. package/dist/_dev-env.d.ts +1 -1
  10. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
  11. package/dist/_dev-vite-config.d.ts +3 -3
  12. package/dist/_e2e-test-utils.d.ts +1 -1
  13. package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
  14. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  15. package/dist/_output.d.ts +10 -9
  16. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  17. package/dist/_resource-commands.d.ts +9 -9
  18. package/dist/_server-common-De0haHr9.mjs +70 -0
  19. package/dist/_server-common.d.ts +20 -1
  20. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  21. package/dist/_slug-api.d.ts +19 -0
  22. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  23. package/dist/_studio-commands.d.ts +13 -13
  24. package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
  25. package/dist/_templates.d.ts +1 -1
  26. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  27. package/dist/_typecheck-gate.d.ts +3 -1
  28. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  29. package/dist/_utils.d.ts +35 -0
  30. package/dist/_vercel-output.d.ts +63 -0
  31. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  32. package/dist/_workflow-determinism.d.ts +3 -3
  33. package/dist/build-BhEaxBPu.mjs +481 -0
  34. package/dist/build.d.ts +23 -10
  35. package/dist/cli.d.ts +1 -2
  36. package/dist/cli.mjs +155 -138
  37. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
  38. package/dist/client-bundler.d.ts +1 -1
  39. package/dist/client-bundler.mjs +1 -1
  40. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  41. package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
  42. package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
  43. package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
  44. package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
  45. package/dist/init.d.ts +14 -0
  46. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  47. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  48. package/dist/project-config.mjs +1 -1
  49. package/dist/scaffold/CLAUDE.md +155 -76
  50. package/dist/scaffold/package.json +6 -6
  51. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  52. package/dist/secret.d.ts +8 -6
  53. package/dist/start.d.ts +112 -0
  54. package/dist/start.mjs +156 -0
  55. package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
  56. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  57. package/dist/templates/call-audit/agent.test.ts +11 -6
  58. package/dist/templates/call-audit/agent.ts +2 -2
  59. package/dist/templates/call-audit/client.tsx +27 -42
  60. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  61. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  62. package/dist/templates/call-audit/workflows/media.ts +12 -37
  63. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  64. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  65. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  66. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  67. package/dist/templates/dispatch-center/client.tsx +53 -61
  68. package/dist/templates/dispatch-center/shared.ts +0 -8
  69. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  70. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  71. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  72. package/dist/templates/infocom-adventure/client.tsx +40 -36
  73. package/dist/templates/link-digest/agent.test.ts +3 -3
  74. package/dist/templates/link-digest/agent.ts +2 -2
  75. package/dist/templates/link-digest/client.tsx +81 -19
  76. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  77. package/dist/templates/math-buddy/agent.test.ts +126 -0
  78. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  79. package/dist/templates/night-owl/client.tsx +8 -2
  80. package/dist/templates/personal-finance/agent.test.ts +127 -0
  81. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  82. package/dist/templates/pizza-ordering/client.tsx +5 -4
  83. package/dist/templates/pizza-ordering/shared.ts +3 -8
  84. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  85. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  86. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  87. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  88. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  89. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  90. package/dist/templates/plan-and-execute/client.tsx +2 -2
  91. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  92. package/dist/templates/podcast-digest/client.tsx +16 -22
  93. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  94. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  95. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  96. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  97. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  98. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  99. package/dist/templates/redline/agent.eval.test.ts +5 -8
  100. package/dist/templates/redline/agent.test.ts +1 -1
  101. package/dist/templates/redline/client.tsx +36 -34
  102. package/dist/templates/redline/workflows/redline.ts +17 -17
  103. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  104. package/dist/templates/research-workflow/agent.test.ts +10 -7
  105. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  106. package/dist/templates/retail/agent.test.ts +17 -15
  107. package/dist/templates/retail/cancel.ts +7 -6
  108. package/dist/templates/retail/client.tsx +39 -58
  109. package/dist/templates/retail/payment.ts +4 -3
  110. package/dist/templates/retail/pending.test.ts +19 -19
  111. package/dist/templates/retail/store.test.ts +5 -5
  112. package/dist/templates/retail/swap.ts +8 -11
  113. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  114. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  115. package/dist/templates/solo-rpg/client.tsx +3 -3
  116. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  117. package/dist/templates/spoken-summary/agent.ts +3 -3
  118. package/dist/templates/spoken-summary/client.tsx +23 -30
  119. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  120. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  121. package/dist/templates/support-line/agent.ts +8 -0
  122. package/dist/templates/support-line/client.tsx +4 -3
  123. package/dist/templates/support-line/prompts.ts +0 -2
  124. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  125. package/dist/templates/transcription-workflow/agent.ts +2 -2
  126. package/dist/templates/transcription-workflow/client.tsx +15 -14
  127. package/dist/templates/transcription-workflow/recover.ts +1 -1
  128. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  129. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  130. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  131. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  132. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  133. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  134. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  135. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  136. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  137. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  138. package/dist/templates/travel-concierge/client.tsx +2 -2
  139. package/dist/templates/travel-concierge/routing.ts +64 -55
  140. package/dist/templates/travel-concierge/shared.ts +10 -14
  141. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  142. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  143. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  144. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  145. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  146. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  147. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  148. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  149. package/dist/templates/web-researcher/agent.test.ts +130 -0
  150. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  151. package/dist/typecheck.mjs +1 -1
  152. package/dist/worker-bundler.d.ts +1 -1
  153. package/dist/worker-bundler.mjs +8 -8
  154. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  155. package/package.json +9 -4
  156. package/dist/_server-common-BV4_CrP3.mjs +0 -43
  157. package/dist/build-CWyFGe-W.mjs +0 -69
  158. package/dist/scaffold/server.mjs +0 -204
@@ -76,9 +76,11 @@ import "@alexkroman1/aai-ui/styles.css";
76
76
  // already declares.
77
77
  import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
78
78
  import {
79
+ BulletList,
79
80
  createWorkflowApi,
81
+ Facts,
80
82
  Form,
81
- page,
83
+ mountPage,
82
84
  SubmitButton,
83
85
  UploadProgressBar,
84
86
  useDownloadUrl,
@@ -86,7 +88,6 @@ import {
86
88
  WorkflowFields,
87
89
  WorkflowProgress,
88
90
  } from "@alexkroman1/aai-ui";
89
- import { useState } from "react";
90
91
  import type { audit } from "./agent.ts";
91
92
 
92
93
  /**
@@ -130,33 +131,13 @@ function Stat({ label, value }: { label: string; value: string }) {
130
131
  );
131
132
  }
132
133
 
133
- /** A list that renders nothing rather than an empty box — see `risks` in the schema. */
134
- function Findings({ title, items }: { title: string; items: string[] }) {
135
- if (items.length === 0) return null;
136
- return (
137
- <section className="flex flex-col gap-1">
138
- <h3 className="text-sm font-medium opacity-70">{title}</h3>
139
- <ul className="flex list-disc flex-col gap-1 pl-5">
140
- {items.map((item) => (
141
- <li key={item}>{item}</li>
142
- ))}
143
- </ul>
144
- </section>
145
- );
146
- }
147
-
148
134
  export function App() {
149
- // Did THIS load start the run? A reload cannot have, and that is the only way
150
- // the page can tell "auditing what you just uploaded" from "picking up where
151
- // you left off" — the hook reports the run, not who asked for it.
152
- const [startedHere, setStartedHere] = useState(false);
153
135
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
154
136
  // `run.output` instead of `unknown`. The reload — both halves of it — is the
155
137
  // hook's own doing; see the module doc for why the key it mints is the one
156
138
  // this desk wants.
157
- const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
158
- typeof audit
159
- >(WORKFLOW, { api });
139
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
140
+ useWorkflowSubmit<typeof audit>(WORKFLOW, { api });
160
141
  const output = run?.status === "completed" ? run.output : undefined;
161
142
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
162
143
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -173,14 +154,7 @@ export function App() {
173
154
  </p>
174
155
  </header>
175
156
 
176
- <Form
177
- onSubmit={(values) => {
178
- setStartedHere(true);
179
- return submitForm(values);
180
- }}
181
- error={error}
182
- className="flex flex-col gap-4"
183
- >
157
+ <Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
184
158
  {/* Every control, from the workflow's own input schema. See the module doc. */}
185
159
  <WorkflowFields workflow={WORKFLOW} />
186
160
  <SubmitButton pending={pending} pendingLabel="Auditing…">
@@ -200,20 +174,26 @@ export function App() {
200
174
  for `<WorkflowProgress>` to read. */}
201
175
  <UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
202
176
 
203
- {/* What the run itself says, from `report()` in the workflow's steps — which
177
+ {/* What the run itself says, from `stepReport()` in the workflow's steps — which
204
178
  for this template is the ffmpeg narration: what the file was, what it
205
179
  measured, how many pauses were found. */}
206
180
  <WorkflowProgress runId={run?.runId} api={api} />
207
181
 
208
- {run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
182
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error: this
183
+ is the outcome the reader waited minutes for. */}
184
+ {run?.status === "failed" && (
185
+ <p role="alert" className="text-red-600">
186
+ That one failed: {run.error}
187
+ </p>
188
+ )}
209
189
 
210
190
  {output !== undefined && (
211
191
  <article className="flex flex-col gap-6">
212
192
  <div className="flex flex-col gap-1">
213
193
  <h2 className="text-xl">{output.headline}</h2>
214
- <p className="text-sm opacity-70">
215
- {output.source} · {formatDuration(output.durationMs)} · {output.words} words
216
- </p>
194
+ <Facts
195
+ items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
196
+ />
217
197
  </div>
218
198
 
219
199
  {/* What the pipeline did, which is this template's subject. Rendered rather
@@ -231,8 +211,11 @@ export function App() {
231
211
  <Stat label="Run time" value={formatDuration(output.elapsedMs)} />
232
212
  </dl>
233
213
 
234
- <Findings title="Risks" items={output.risks} />
235
- <Findings title="Actions" items={output.actions} />
214
+ {/* Either list can come back empty — see `risks` in the schema — and
215
+ `<BulletList>` renders nothing at all when it does, heading
216
+ included, rather than a stray heading over an empty box. */}
217
+ <BulletList title="Risks" items={output.risks} />
218
+ <BulletList title="Actions" items={output.actions} />
236
219
 
237
220
  <section className="flex flex-col gap-2">
238
221
  <h3 className="text-sm font-medium opacity-70">
@@ -241,7 +224,9 @@ export function App() {
241
224
  </h3>
242
225
  {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
243
226
  {audio.error !== undefined && (
244
- <p className="text-red-600">Could not load the audio: {audio.error}</p>
227
+ <p role="alert" className="text-red-600">
228
+ Could not load the audio: {audio.error}
229
+ </p>
245
230
  )}
246
231
  {audio.url !== undefined && (
247
232
  <>
@@ -251,7 +236,7 @@ export function App() {
251
236
  track would carry. `spoken-summary` serves a one-cue WebVTT
252
237
  data URL instead — worth reading for how, if a real track is
253
238
  what a page needs. */}
254
- <audio controls src={audio.url} className="w-full" />
239
+ <audio aria-label="Audit read aloud" controls src={audio.url} className="w-full" />
255
240
  {/* `download` works on an object URL because the bytes are already in
256
241
  the tab; it is the href that could not carry the agent's bearer,
257
242
  not the attribute. */}
@@ -273,4 +258,4 @@ export function App() {
273
258
  );
274
259
  }
275
260
 
276
- page({ name: "Call Audit", component: App });
261
+ mountPage({ name: "Call Audit", component: App });
@@ -45,8 +45,8 @@
45
45
  * ingest result, once as the plan) and buy nothing.
46
46
  */
47
47
 
48
- import type { WorkflowCtx } from "@alexkroman1/aai";
49
- import { encodeWav, mapConcurrent, readUpload, report } from "@alexkroman1/aai/step";
48
+ import type { WorkflowContext } from "@alexkroman1/aai";
49
+ import { encodeWav, mapConcurrent, stepReadUpload, stepReport } from "@alexkroman1/aai/step";
50
50
  import { countWords, formatDuration } from "@alexkroman1/aai/utils";
51
51
  // ERASED at build time, so the body can name the schema's own output type without
52
52
  // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
@@ -156,7 +156,7 @@ export type CallAudit = {
156
156
  */
157
157
  export async function auditFlow(
158
158
  input: WorkflowInputOf<typeof audit>,
159
- ctx: WorkflowCtx,
159
+ ctx: WorkflowContext,
160
160
  ): Promise<CallAudit> {
161
161
  // Both at once: neither needs the other, and issued together they are one round
162
162
  // trip instead of two before any audio moves. The ORDER is still a pure function
@@ -259,12 +259,14 @@ export async function transcribeSegment(audioId: string, segment: Segment): Prom
259
259
  // ORDER is not guaranteed here and does not need to be — the calls go out
260
260
  // together, so their lines interleave by completion, and `segment.index` is what
261
261
  // puts the TRANSCRIPT back in order.
262
- await report(`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`);
262
+ await stepReport(
263
+ `Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
264
+ );
263
265
 
264
266
  // `[start, end)`, the same half-open pair `planSegments` produced — the store
265
267
  // owns the conversion to HTTP's inclusive range, so there is no `- 1` here to get
266
268
  // wrong.
267
- const audio = await readUpload(audioId, { start: segment.startByte, end: segment.endByte });
269
+ const audio = await stepReadUpload(audioId, { start: segment.startByte, end: segment.endByte });
268
270
  const text = await transcribeSpan(
269
271
  encodeWav(audio.bytes, ANALYSIS_FORMAT),
270
272
  `segment-${segment.index}.wav`,
@@ -63,7 +63,7 @@
63
63
  import { readFile, stat } from "node:fs/promises";
64
64
  import { join } from "node:path";
65
65
  import { probeMedia, runFfmpeg } from "@alexkroman1/aai/ffmpeg";
66
- import { pcmDurationMs, report, requireCompleteUpload } from "@alexkroman1/aai/step";
66
+ import { pcmDurationMs, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
67
67
  import { throwFatalStepError, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
68
68
  import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
69
69
  import { formatBytes, formatDuration, plural } from "@alexkroman1/aai/utils";
@@ -132,10 +132,10 @@ export type Ingested = {
132
132
  // nothing. Removing them is what took the file under the floor: a directive
133
133
  // propping a coverage number up is the least useful statement in the tree.
134
134
  export async function ingestRecording(uploadId: string): Promise<Ingested> {
135
- // `requireCompleteUpload`, not `uploadInfo`: `size` is the readable PREFIX, so
135
+ // `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, so
136
136
  // an upload still arriving would be copied short and levelled as the whole call.
137
- const stored = await requireCompleteUpload(uploadId);
138
- await report(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
137
+ const stored = await stepRequireCompleteUpload(uploadId);
138
+ await stepReport(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
139
139
 
140
140
  return await withTempDir(
141
141
  async (dir) => {
@@ -148,7 +148,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
148
148
  // `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
149
149
  // completeness myself", which is what a body polling a still-arriving
150
150
  // upload needs and is the opposite of what happened above: this step has
151
- // already called `requireCompleteUpload`, so the file IS whole and the
151
+ // already called `stepRequireCompleteUpload`, so the file IS whole and the
152
152
  // windows may land in any order. Omitting it lets `readUploadToFile`
153
153
  // establish that for itself and fan out. The cost is one metadata round
154
154
  // trip, against the dozens of window reads it overlaps.
@@ -162,7 +162,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
162
162
  throwFfmpegStepError,
163
163
  );
164
164
  const codec = probed.audio?.codec ?? "unknown";
165
- await report(
165
+ await stepReport(
166
166
  `Levelling ${describeSource(codec, probed.durationSec)} to ${ANALYSIS_FORMAT.sampleRate / 1000} kHz mono.`,
167
167
  );
168
168
 
@@ -206,7 +206,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
206
206
  type: "application/octet-stream",
207
207
  });
208
208
 
209
- await report(
209
+ await stepReport(
210
210
  `Levelled ${formatDuration(durationMs)} from ${loudness.inputLufs} LUFS, ` +
211
211
  `${Math.round(speechFraction(silences, durationMs / 1000) * 100)}% speech across ` +
212
212
  `${silences.length} ${plural(silences.length, "pause")}.`,
@@ -16,7 +16,7 @@
16
16
  * ## The argv is ours, so it is BUILT rather than embedded
17
17
  *
18
18
  * `runFfmpeg` passes `args` through verbatim — no `-y`, no `-loglevel` — so the
19
- * standing flags are a decision this file makes once ({@link standardFlags}) and
19
+ * standing flags come from `ffmpegBaseArgs` on `@alexkroman1/aai/ffmpeg`, and
20
20
  * every invocation's real argv is a value a test can assert on. It is also why
21
21
  * the filter strings below carry no shell quoting: each is ONE element of an
22
22
  * argv array, so the commas that chain filters and the colons that separate their
@@ -58,7 +58,9 @@
58
58
  * longer than the cap gets the blind cut, and says so.
59
59
  */
60
60
 
61
+ import { ffmpegBaseArgs } from "@alexkroman1/aai/ffmpeg";
61
62
  import type { PcmFormat } from "@alexkroman1/aai/step";
63
+ import { safeJsonParse } from "@alexkroman1/aai/utils";
62
64
  import { z } from "zod";
63
65
 
64
66
  /**
@@ -151,20 +153,6 @@ export const MAX_SEGMENT_SECONDS = 110;
151
153
  */
152
154
  export const MIN_SEGMENT_SECONDS = 1;
153
155
 
154
- /** The standing flags, on every invocation this desk makes. */
155
- export function standardFlags(): string[] {
156
- return [
157
- "-hide_banner",
158
- // Progress lines are noise in a captured stderr, and the SDK keeps only a
159
- // tail of it — so suppressing them is what leaves room for the diagnosis.
160
- "-nostats",
161
- // In a guest there is no terminal, and an ffmpeg that decides to read stdin
162
- // is a process that never exits.
163
- "-nostdin",
164
- "-y",
165
- ];
166
- }
167
-
168
156
  /** A loudness measurement, as `loudnorm`'s first pass reports it. */
169
157
  export type Loudness = {
170
158
  /** Integrated loudness, LUFS. */
@@ -233,9 +221,9 @@ export class MediaAnalysisError extends Error {
233
221
  */
234
222
  export function measureLoudnessArgs(input: string): string[] {
235
223
  return [
236
- ...standardFlags(),
237
- "-loglevel",
238
- "info",
224
+ // `info`, not the default `error`: `print_format=json` reports through the
225
+ // LOG, so at `error` this pass runs, succeeds, and prints nothing.
226
+ ...ffmpegBaseArgs({ loglevel: "info" }),
239
227
  "-i",
240
228
  input,
241
229
  "-af",
@@ -273,7 +261,7 @@ export function parseLoudness(stderr: string): Loudness {
273
261
  // different remedies, and the second message quotes the value ffmpeg actually
274
262
  // printed — which needs the block still in hand, so the gate cannot be folded
275
263
  // into the schema below.
276
- const block = LoudnessBlock.safeParse(safeJson(stderr.slice(open, close + 1)));
264
+ const block = LoudnessBlock.safeParse(safeJsonParse(stderr.slice(open, close + 1)));
277
265
  if (!block.success) {
278
266
  throw new MediaAnalysisError("The loudness pass printed a block that is not JSON.");
279
267
  }
@@ -301,7 +289,7 @@ export function parseLoudness(stderr: string): Loudness {
301
289
  /**
302
290
  * Is the found `{…}` an object at all?
303
291
  *
304
- * The reachable failure is {@link safeJson} answering `undefined` — a brace pair
292
+ * The reachable failure is {@link safeJsonParse} answering `undefined` — a brace pair
305
293
  * found in ffmpeg's chatter with something other than JSON between them — and
306
294
  * that is a different sentence from a value being unreadable, which is why this
307
295
  * gate exists at all rather than being folded into {@link LoudnessValues}.
@@ -370,12 +358,10 @@ export function normalizeArgs(
370
358
  ].join(":");
371
359
 
372
360
  return [
373
- ...standardFlags(),
374
361
  // Quiet, and the analysis still arrives: `ametadata` writes its file
375
362
  // directly rather than through the log, which is the property that lets this
376
363
  // pass be both silent and complete.
377
- "-loglevel",
378
- "error",
364
+ ...ffmpegBaseArgs(),
379
365
  "-i",
380
366
  input,
381
367
  "-af",
@@ -456,7 +442,7 @@ export function parseSilences(log: string, durationSec: number): Silence[] {
456
442
  * Exact rather than rounded, and that distinction cost a bug: `pcmDurationMs`
457
443
  * answers whole MILLISECONDS, so a 640,500-byte file reports 20,016 ms where it
458
444
  * really holds 20,015.625. Planning from the rounded number put the last segment's
459
- * `endByte` at 640,512 — twelve bytes past the end of the file. `readUpload` clamps
445
+ * `endByte` at 640,512 — twelve bytes past the end of the file. `stepReadUpload` clamps
460
446
  * a window to the stored size, so nothing threw; the plan was simply describing
461
447
  * audio that does not exist. Verified against a real ffmpeg, which is the only
462
448
  * place a 12-byte error was ever going to show up.
@@ -474,7 +460,7 @@ export function durationSeconds(totalBytes: number): number {
474
460
  * Greedy from the front: a segment grows until the next cut candidate would take
475
461
  * it past {@link MAX_SEGMENT_SECONDS}, so it ends at the LAST pause that still
476
462
  * fits. Segments are therefore contiguous and non-overlapping — together they are
477
- * the whole recording, each one addressable as a single `readUpload` window.
463
+ * the whole recording, each one addressable as a single `stepReadUpload` window.
478
464
  *
479
465
  * Three properties, each of which a simpler version gets wrong:
480
466
  *
@@ -600,9 +586,7 @@ export function planSegments(silences: readonly Silence[], totalBytes: number):
600
586
  */
601
587
  export function masterArgs(input: string, output: string): string[] {
602
588
  return [
603
- ...standardFlags(),
604
- "-loglevel",
605
- "error",
589
+ ...ffmpegBaseArgs(),
606
590
  "-i",
607
591
  input,
608
592
  "-af",
@@ -652,12 +636,3 @@ function value(line: string, key: string): number | undefined {
652
636
  const parsed = Number(text);
653
637
  return Number.isFinite(parsed) ? parsed : undefined;
654
638
  }
655
-
656
- /** `JSON.parse` that answers `undefined` rather than throwing, so the caller frames the error. */
657
- function safeJson(text: string): unknown {
658
- try {
659
- return JSON.parse(text);
660
- } catch {
661
- return undefined;
662
- }
663
- }
@@ -11,7 +11,7 @@
11
11
  * `spoken-summary` owns the audio ROUND TRIP and is the template to read for it:
12
12
  * why `stepSpeak` exists at all (a `TtsSession` is an event stream wired into a
13
13
  * live pipeline's playback, and a step has no turn to be part of and has to return
14
- * a VALUE), why `writeUpload` is its other half, and why speaking and storing must
14
+ * a VALUE), why `stepWriteUpload` is its other half, and why speaking and storing must
15
15
  * be one step. None of that is restated here.
16
16
  *
17
17
  * **What this file adds is the pass AFTER the synthesis**, and it is the second
@@ -35,8 +35,8 @@
35
35
  import { stat, writeFile } from "node:fs/promises";
36
36
  import { join } from "node:path";
37
37
  import { runFfmpeg } from "@alexkroman1/aai/ffmpeg";
38
- import { report, stepSpeak } from "@alexkroman1/aai/step";
39
- import { stepGenerateJsonClassified, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
38
+ import { stepReport, stepSpeak } from "@alexkroman1/aai/step";
39
+ import { stepGenerateJsonOrFail, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
40
40
  import { withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
41
41
  import { formatBytes, formatDuration, omitUndefined, plural } from "@alexkroman1/aai/utils";
42
42
  import { z } from "zod";
@@ -105,8 +105,8 @@ export async function summarize(
105
105
  source: string,
106
106
  durationMs: number,
107
107
  ): Promise<CallSummary> {
108
- await report("Reading the transcript.");
109
- const reply = await stepGenerateJsonClassified(
108
+ await stepReport("Reading the transcript.");
109
+ const reply = await stepGenerateJsonOrFail(
110
110
  `Audit this transcript of a recorded call (${source}, ${formatDuration(durationMs)}).\n\n` +
111
111
  "Answer with JSON only, in this shape:\n" +
112
112
  `{"headline": "...", "risks": ["..."], "actions": ["..."], "spoken": "..."}\n\n` +
@@ -125,12 +125,12 @@ export async function summarize(
125
125
  system: "You audit recorded calls. You answer with JSON and nothing else.",
126
126
  schema: AuditReply,
127
127
  },
128
- // The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which is
128
+ // The `OrFail` caller is `stepGenerateJson` plus `throwStepError`, which is
129
129
  // what reads the gateway's own status: a 429 is worth another attempt and a 400
130
130
  // is not, and that is what tells the DevKit which.
131
131
  );
132
132
 
133
- await report(
133
+ await stepReport(
134
134
  `Found ${reply.risks.length} ${plural(reply.risks.length, "risk")} and ` +
135
135
  `${reply.actions.length} ${plural(reply.actions.length, "action")}.`,
136
136
  );
@@ -186,7 +186,7 @@ export async function narrate(
186
186
  type: "audio/mpeg",
187
187
  });
188
188
 
189
- await report(
189
+ await stepReport(
190
190
  `Recorded a ${Math.round(spoken.durationMs / 1000)}s audit in ${spoken.voice}'s voice — ` +
191
191
  `${formatBytes(bytes)} of MP3, from ${formatBytes(spoken.audio.byteLength)} of WAV.`,
192
192
  );
@@ -11,7 +11,7 @@
11
11
  * the DevKit, and that belongs somewhere a spec can reach it.
12
12
  */
13
13
 
14
- import { stepTranscribeSyncClassified } from "@alexkroman1/aai/step-errors";
14
+ import { stepTranscribeSyncOrFail } from "@alexkroman1/aai/step-errors";
15
15
 
16
16
  /**
17
17
  * Transcribe one complete WAV.
@@ -21,7 +21,7 @@ import { stepTranscribeSyncClassified } from "@alexkroman1/aai/step-errors";
21
21
  * stores headerless PCM on purpose (see `media.ts`) and puts a header back with
22
22
  * `encodeWav` for exactly this call.
23
23
  *
24
- * `stepTranscribeSyncClassified` — the SDK's own `stepTranscribeSync` plus
24
+ * `stepTranscribeSyncOrFail` — the SDK's own `stepTranscribeSync` plus
25
25
  * `throwStepError`, and nothing else — is the whole of what this adds, and it is
26
26
  * where the three-way call is made: a `FatalError` stops the DevKit retrying something that
27
27
  * will answer the same way, a bare `RetryableError` retries in ONE SECOND (that
@@ -39,6 +39,6 @@ export async function transcribeSpan(
39
39
  filename: string,
40
40
  label: string,
41
41
  ): Promise<string> {
42
- const { text } = await stepTranscribeSyncClassified(bytes, { filename, label });
42
+ const { text } = await stepTranscribeSyncOrFail(bytes, { filename, label });
43
43
  return text;
44
44
  }
@@ -0,0 +1,103 @@
1
+ /** The def a DEPLOYED agent runs: authored, plus the `system-prompt.md` beside it. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { AgentConfigSchema, toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { describe, expect, test } from "vitest";
5
+
6
+ /**
7
+ * What this starter's spec may assert.
8
+ *
9
+ * Renaming Coda, giving her a voice, swapping a stage or switching the whole
10
+ * thing to speech-to-speech are the first edits this template invites — and
11
+ * `aai build` runs these tests before it bundles, so an assertion that pins the
12
+ * template's own identity turns the first customization into a build failure in
13
+ * a file the author never wrote. Every test here asserts a property that
14
+ * survives those edits, on the RESOLVED config rather than on the def's empty
15
+ * fields.
16
+ *
17
+ * What it may NOT assert is whether `run_code` actually RUNS: the builtin is
18
+ * sandbox-only, so off-platform it declines rather than evaluating
19
+ * model-written JavaScript in the host process. That is right, and it leaves
20
+ * "Coda reached for code, and the code came back with 107823" to
21
+ * `agent.eval.test.ts`, which supplies an executor of its own. What is reachable
22
+ * in memory is the pairing the template is built on — the builtin it asks for
23
+ * and the prompt that commands it — and that is what the last two tests are.
24
+ */
25
+ describe("code-interpreter template", () => {
26
+ test("config passes manifest validation", () => {
27
+ // Same conversion `aai build`/`aai deploy` run.
28
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
29
+ });
30
+
31
+ test("exports an agent the platform can name", () => {
32
+ // Not the literal: what has to hold is that there IS a name and that the
33
+ // conversion carries it through — `AgentName` refuses a blank one, and the
34
+ // studio lists a deployed agent by exactly this string.
35
+ expect(agentDef.name).toBeTruthy();
36
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
37
+ });
38
+
39
+ test("whatever mode it ends up in, there is a model to call the tool with", () => {
40
+ // A builtin is something a MODEL reaches for, so this template is only
41
+ // itself while some stage can issue a tool call. Asserted per MODE so it
42
+ // survives the swaps above: declare `stt`/`llm`/`tts` and the rest still
43
+ // default to the all-AssemblyAI cascade (which is why the def declares no
44
+ // provider at all and still runs); declare `s2s` and there is no cascade to
45
+ // fill, which is the one thing that must never happen by fallthrough.
46
+ const config = toAgentConfig(agentDef);
47
+ if (config.mode === "s2s") {
48
+ expect(config.s2s?.kind).toBeTruthy();
49
+ expect(config.stt).toBeUndefined();
50
+ expect(config.tts).toBeUndefined();
51
+ return;
52
+ }
53
+ expect(config.llm?.kind).toBeTruthy();
54
+ if (config.mode === "pipeline") {
55
+ expect(config.stt?.kind).toBeTruthy();
56
+ expect(config.tts?.kind).toBeTruthy();
57
+ }
58
+ });
59
+
60
+ test("run_code survives into the config a deploy carries", () => {
61
+ // The template's whole capability, asserted on the CONFIG rather than the
62
+ // def because that is what a deploy ships. `DEFAULT_BUILTIN_TOOLS` is
63
+ // EMPTY — a builtin is something an agent asks for, never something it has
64
+ // to notice and switch off — so a dropped `builtinTools` is not a degraded
65
+ // Coda, it is an agent whose prompt forbids mental arithmetic and leaves it
66
+ // nothing else to do. Adding builtins beside it is fine; losing this one is
67
+ // the regression.
68
+ expect(toAgentConfig(agentDef).builtinTools ?? []).toContain("run_code");
69
+ });
70
+
71
+ test("every builtin the prompt commands by name is one the agent declares", () => {
72
+ // The pairing that makes this template work, and the failure it catches is
73
+ // silent in both directions: a prompt commanding `fetch_json` at an agent
74
+ // that never declared it produces a model apologizing for a tool it cannot
75
+ // see, and a builtin renamed in `agent.ts` alone leaves the CRITICAL RULES
76
+ // addressed to nothing. Neither shows up in a diff of either file.
77
+ const config = toAgentConfig(agentDef);
78
+ const declared = config.builtinTools ?? [];
79
+
80
+ // Which snake_case tokens in the prose are TOOL NAMES is a question for the
81
+ // SDK's own schema, not for a list restated here: a catalog copied into a
82
+ // spec goes stale, and matching every underscored word would redden on a
83
+ // prompt that names a variable in one of its examples.
84
+ const isBuiltin = (name: string) =>
85
+ AgentConfigSchema.safeParse({ ...config, builtinTools: [name] }).success;
86
+ const commanded = [
87
+ ...new Set(config.systemPrompt.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) ?? []),
88
+ ].filter(isBuiltin);
89
+
90
+ // Non-vacuity, and it earns its keep twice: a prompt naming no builtin at
91
+ // all would make the loop below assert nothing, and it is also the state a
92
+ // template gets into when `system-prompt.md` is not applied — the framework
93
+ // default names no builtin, so an agent running on it lands here rather
94
+ // than passing quietly with the CRITICAL RULES nowhere in its context.
95
+ expect(commanded.length).toBeGreaterThan(0);
96
+ for (const name of commanded) {
97
+ expect(declared, `the prompt commands ${name}`).toContain(name);
98
+ }
99
+ // The converse is deliberately NOT asserted: declaring a builtin the prompt
100
+ // never mentions is an ordinary edit, and the model is told about it by its
101
+ // own tool schema.
102
+ });
103
+ });
@@ -6,7 +6,7 @@ import type {
6
6
  ToolInputSchema,
7
7
  } from "@alexkroman1/aai";
8
8
  import { isToolFailure } from "@alexkroman1/aai";
9
- import { createToolContext, ok } from "@alexkroman1/aai/testing";
9
+ import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
10
10
  import { describe, expect, test } from "vitest";
11
11
  import { callFlow, dispatchSlot } from "./shared.ts";
12
12
  import incidentAddNote from "./tools/incident_add_note.ts";
@@ -32,7 +32,7 @@ const makeCtx = (): ToolContext => createToolContext();
32
32
  * shapes the assertions below used to restate, which were a second copy of each
33
33
  * tool's return type that could not go stale loudly.
34
34
  *
35
- * The unwrap itself is `ok` from `@alexkroman1/aai/testing`; the hand-rolled
35
+ * The unwrap itself is `expectToolOk` from `@alexkroman1/aai/testing`; the hand-rolled
36
36
  * copy that used to sit here was byte-identical to three other templates'.
37
37
  */
38
38
  type Result<T extends ToolDef<ToolInputSchema>> = Extract<
@@ -78,7 +78,7 @@ describe("dispatch-center template", () => {
78
78
  const ctx = makeCtx();
79
79
  const incidentId = await createIncidentFor(ctx, "cardiac arrest, patient not breathing");
80
80
 
81
- const result = ok<Result<typeof resourcesDispatch>>(
81
+ const result = expectToolOk<Result<typeof resourcesDispatch>>(
82
82
  await resourcesDispatch.execute({ incidentId, callsigns: ["auto"] }, ctx),
83
83
  );
84
84
 
@@ -205,13 +205,13 @@ describe("the call flow", () => {
205
205
  expect(created.state).toBe("working.triaging");
206
206
  expect(created.instruction).toMatch(/incident_triage/);
207
207
 
208
- const triaged = ok<Result<typeof incidentTriage>>(
208
+ const triaged = expectToolOk<Result<typeof incidentTriage>>(
209
209
  await incidentTriage.execute({ incidentId: created.incidentId, severity: "critical" }, ctx),
210
210
  );
211
211
  expect(triaged.triageScore).toBeGreaterThan(0);
212
212
  expect(at(ctx).state).toBe("working.dispatching");
213
213
 
214
- ok(
214
+ expectToolOk(
215
215
  await resourcesDispatch.execute({ incidentId: created.incidentId, autoDispatch: true }, ctx),
216
216
  );
217
217
  expect(at(ctx).state).toBe("working.monitoring");
@@ -226,7 +226,7 @@ describe("the call flow", () => {
226
226
 
227
227
  // Every requested callsign is unknown, so no unit moved — and the call has
228
228
  // not moved on either.
229
- const result = ok<Result<typeof resourcesDispatch>>(
229
+ const result = expectToolOk<Result<typeof resourcesDispatch>>(
230
230
  await resourcesDispatch.execute({ incidentId, callsigns: ["Ghost-1"] }, ctx),
231
231
  );
232
232
  expect(result.dispatched).toHaveLength(0);
@@ -245,7 +245,7 @@ describe("the call flow", () => {
245
245
 
246
246
  // The first incident is still workable — the position tracks the call in
247
247
  // hand, and the tools are addressed by id.
248
- ok(await incidentAddNote.execute({ incidentId: first, note: "crews on scene" }, ctx));
248
+ expectToolOk(await incidentAddNote.execute({ incidentId: first, note: "crews on scene" }, ctx));
249
249
  });
250
250
 
251
251
  test("a failed tool does not advance the flow", async () => {