@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.
- package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/_artifacts-BJOYGQPp.mjs +21 -0
- package/dist/_artifacts.d.ts +16 -0
- package/dist/_build-target.d.ts +172 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
- package/dist/_templates.d.ts +1 -1
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +23 -10
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +155 -138
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
- package/dist/init.d.ts +14 -0
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +155 -76
- package/dist/scaffold/package.json +6 -6
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/code-interpreter/agent.test.ts +103 -0
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +81 -19
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/agent.ts +8 -0
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/routing.ts +64 -55
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
- package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/templates/web-researcher/agent.test.ts +130 -0
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +8 -8
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +9 -4
- package/dist/_server-common-BV4_CrP3.mjs +0 -43
- package/dist/build-CWyFGe-W.mjs +0 -69
- 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
|
-
|
|
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 } =
|
|
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 `
|
|
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
|
-
{
|
|
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
|
-
<
|
|
215
|
-
{output.source
|
|
216
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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">
|
|
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
|
-
|
|
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 {
|
|
49
|
-
import { encodeWav, mapConcurrent,
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
// `
|
|
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
|
|
138
|
-
await
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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. `
|
|
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 `
|
|
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
|
-
...
|
|
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 `
|
|
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 {
|
|
39
|
-
import {
|
|
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
|
|
109
|
-
const reply = await
|
|
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 `
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
* `
|
|
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
|
|
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,
|
|
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 `
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 () => {
|