@alexkroman1/aai-cli 9.1.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
- package/dist/_bundler.d.ts +3 -41
- package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
- package/dist/_dev-server.d.ts +1 -8
- package/dist/_dev-vite-config.d.ts +21 -0
- package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
- package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
- package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
- package/dist/_templates.d.ts +29 -0
- package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
- package/dist/cli.mjs +46 -22
- package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
- package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
- package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
- package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
- package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +162 -54
- package/dist/scaffold/package.json +4 -6
- package/dist/scaffold/server.mjs +12 -23
- package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
- package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
- package/dist/templates/call-audit/agent.eval.test.ts +1 -1
- package/dist/templates/call-audit/agent.test.ts +80 -8
- package/dist/templates/call-audit/client.tsx +87 -3
- package/dist/templates/call-audit/workflows/audit.ts +52 -32
- package/dist/templates/call-audit/workflows/ingest.ts +15 -15
- package/dist/templates/call-audit/workflows/media.ts +1 -1
- package/dist/templates/call-audit/workflows/summarize.ts +0 -4
- package/dist/templates/link-digest/agent.eval.test.ts +4 -2
- package/dist/templates/link-digest/agent.test.ts +25 -5
- package/dist/templates/link-digest/client.tsx +65 -7
- package/dist/templates/link-digest/workflows/digest.ts +39 -29
- package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
- package/dist/templates/podcast-digest/agent.test.ts +108 -2
- package/dist/templates/podcast-digest/client.tsx +89 -8
- package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
- package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
- package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
- package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
- package/dist/templates/recap-workflow/agent.test.ts +323 -83
- package/dist/templates/recap-workflow/shared.ts +4 -3
- package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
- package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
- package/dist/templates/redline/agent.eval.test.ts +1 -1
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +83 -3
- package/dist/templates/redline/workflows/redline.ts +26 -23
- package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
- package/dist/templates/research-workflow/agent.test.ts +32 -6
- package/dist/templates/research-workflow/agent.ts +4 -4
- package/dist/templates/research-workflow/shared.ts +4 -3
- package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
- package/dist/templates/research-workflow/workflows/research.ts +35 -38
- package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +10 -8
- package/dist/templates/spoken-summary/client.tsx +72 -3
- package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
- package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
- package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
- package/dist/templates/transcription-workflow/agent.test.ts +720 -16
- package/dist/templates/transcription-workflow/agent.ts +6 -4
- package/dist/templates/transcription-workflow/client.tsx +106 -308
- package/dist/templates/transcription-workflow/recover.test.ts +141 -0
- package/dist/templates/transcription-workflow/recover.ts +130 -0
- package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
- package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
- package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
- package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
- package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
- package/dist/templates/transcription-workflow/workflows/stream.ts +311 -38
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
- package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
- package/dist/worker-bundler.d.ts +0 -14
- package/dist/worker-bundler.mjs +149 -1
- package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
- package/dist/workflow.d.ts +1 -0
- package/package.json +4 -6
- package/dist/_workflow-scan.d.ts +0 -108
- package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
- package/dist/workflow-bundler.d.ts +0 -138
- package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* ## Batch polling, which is this file's one genuinely new mechanism
|
|
40
40
|
*
|
|
41
41
|
* `spoken-summary` and `transcription-workflow` each wait for ONE transcript,
|
|
42
|
-
* so their poll loop is `for (…) { if (done) return; await sleep(…) }`. Here N
|
|
42
|
+
* so their poll loop is `for (…) { if (done) return; await ctx.sleep(…) }`. Here N
|
|
43
43
|
* episodes are in flight at once and they finish out of order, so the loop has
|
|
44
44
|
* to carry a shrinking pending set and let the finished ones drop out — see
|
|
45
45
|
* {@link waitForTranscripts}. It is the same idea one dimension up, and the
|
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
* @module digest
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import type { WorkflowCtx } from "@alexkroman1/aai";
|
|
52
53
|
import { mapConcurrent, report, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
53
54
|
import {
|
|
55
|
+
FatalError,
|
|
54
56
|
stepGenerateJsonClassified,
|
|
55
57
|
stepTranscribePollClassified,
|
|
56
58
|
stepTranscribeSubmitClassified,
|
|
57
59
|
} from "@alexkroman1/aai/step-errors";
|
|
58
60
|
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
59
61
|
import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
|
|
60
|
-
import { FatalError, sleep } from "workflow";
|
|
61
62
|
import { z } from "zod";
|
|
62
63
|
import type { dailyDigest } from "../agent.ts";
|
|
63
64
|
import { discoverEpisodes, type Episode } from "./feeds.ts";
|
|
@@ -70,7 +71,7 @@ import { sendDigestToSlack } from "./slack.ts";
|
|
|
70
71
|
* restating the number — an eval skips the sleep, so the duration asked for is
|
|
71
72
|
* the only observable there is.
|
|
72
73
|
*/
|
|
73
|
-
export const
|
|
74
|
+
export const POLL_DELAY_MS = 20_000;
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* 180 rounds x 20s = an hour, which is far past any podcast episode.
|
|
@@ -93,15 +94,17 @@ const MAX_TRANSCRIPT_CHARS = 18_000;
|
|
|
93
94
|
/**
|
|
94
95
|
* Submissions in flight at once — polite to the provider, still parallel.
|
|
95
96
|
*
|
|
96
|
-
* Inside
|
|
97
|
-
*
|
|
98
|
-
* not the concurrency" in `mapConcurrent`.
|
|
97
|
+
* Inside `DEFAULT_STEP_CONCURRENCY` (`aai-runtime`, 16), so this width is what
|
|
98
|
+
* really runs — see "The WINDOW is not the concurrency" in `mapConcurrent`.
|
|
99
99
|
*/
|
|
100
100
|
const SUBMIT_CONCURRENCY = 2;
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
/**
|
|
103
|
+
* Status checks in flight at once. Cheaper calls, so a wider gate.
|
|
104
|
+
*
|
|
105
|
+
* Well inside `DEFAULT_STEP_CONCURRENCY` (`aai-runtime`, 16), like
|
|
106
|
+
* {@link SUBMIT_CONCURRENCY} above, so this width is what really runs.
|
|
107
|
+
*/
|
|
105
108
|
const POLL_CONCURRENCY = 3;
|
|
106
109
|
|
|
107
110
|
/**
|
|
@@ -173,9 +176,10 @@ const SummaryReply = z.object({
|
|
|
173
176
|
* that would answer differently on the second pass — {@link timestamp} is a step
|
|
174
177
|
* for exactly that reason.
|
|
175
178
|
*/
|
|
176
|
-
export async function dailyDigestFlow(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
export async function dailyDigestFlow(
|
|
180
|
+
input: DigestInput,
|
|
181
|
+
ctx: WorkflowCtx,
|
|
182
|
+
): Promise<DailyDigestOutput> {
|
|
179
183
|
// No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
|
|
180
184
|
// `.default()` has already run by the time a run reaches this line. The
|
|
181
185
|
// chain this replaces restated all four of them, which is a second place for
|
|
@@ -188,26 +192,42 @@ export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOu
|
|
|
188
192
|
let digestsSent = 0;
|
|
189
193
|
|
|
190
194
|
for (let digestNumber = 1; digestNumber <= totalDigests; digestNumber += 1) {
|
|
191
|
-
const episodes = await discoverEpisodes
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
const episodes = await ctx.step("discoverEpisodes", () =>
|
|
196
|
+
discoverEpisodes(input.podcastChannels, maxEpisodes),
|
|
197
|
+
);
|
|
198
|
+
// `maxAttempts` was a `maxRetries` property on each function (4, 4, 5 —
|
|
199
|
+
// retries AFTER the first attempt, so 5, 5, 6 in all). It is an argument to
|
|
200
|
+
// the CALL now, which is where a policy belongs: the same function called
|
|
201
|
+
// from two places may deserve different patience.
|
|
202
|
+
const jobs = await mapConcurrent(episodes, SUBMIT_CONCURRENCY, (episode) =>
|
|
203
|
+
ctx.step("submitTranscript", () => submitTranscript(episode), { maxAttempts: 5 }),
|
|
204
|
+
);
|
|
205
|
+
const transcripts = await waitForTranscripts(jobs, ctx);
|
|
206
|
+
const digests = await mapConcurrent(transcripts, SUBMIT_CONCURRENCY, (transcript) =>
|
|
207
|
+
ctx.step("summarizeTranscript", () => summarizeTranscript(transcript), { maxAttempts: 6 }),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const slackStatus = await ctx.step("postDigest", () =>
|
|
211
|
+
sendDigestToSlack({
|
|
212
|
+
slackWebhookUrl: input.slackWebhookUrl,
|
|
213
|
+
slackWorkflowTextParam: input.slackWorkflowTextParam,
|
|
214
|
+
podcastChannels: input.podcastChannels,
|
|
215
|
+
episodes: digests,
|
|
216
|
+
digestNumber,
|
|
217
|
+
totalDigests,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
lastDigest = {
|
|
222
|
+
sentAt: await ctx.step("timestamp", () => timestamp()),
|
|
223
|
+
slackStatus,
|
|
200
224
|
episodes: digests,
|
|
201
|
-
|
|
202
|
-
totalDigests,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
lastDigest = { sentAt: await timestamp(), slackStatus, episodes: digests };
|
|
225
|
+
};
|
|
206
226
|
digestsSent += 1;
|
|
207
227
|
|
|
208
228
|
// Not after the last one: a run that has delivered everything it owes
|
|
209
229
|
// should end, not sleep for a day and then end.
|
|
210
|
-
if (digestNumber < totalDigests) await sleep(intervalMs);
|
|
230
|
+
if (digestNumber < totalDigests) await ctx.sleep(intervalMs);
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
return {
|
|
@@ -223,10 +243,10 @@ export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOu
|
|
|
223
243
|
/**
|
|
224
244
|
* Wait for a whole BATCH of transcripts, letting them finish out of order.
|
|
225
245
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
246
|
+
* Part of the BODY rather than a step, and not because it is small: it CALLS
|
|
247
|
+
* steps and it sleeps, neither of which may happen inside one. So it is replayed
|
|
248
|
+
* with the body — legal for the ordinary reason, that every line is either a
|
|
249
|
+
* `ctx.step` or a `ctx.sleep` — and it takes the `ctx` for that reason.
|
|
230
250
|
*
|
|
231
251
|
* The shape to notice is that `pending` SHRINKS. A loop that waited for all N
|
|
232
252
|
* on every round would hold the whole batch hostage to its slowest member, and
|
|
@@ -247,19 +267,24 @@ export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOu
|
|
|
247
267
|
* the top. Found by `agent.eval.test.ts`, which is the only tier that can see
|
|
248
268
|
* it, a per-step spec having no batch to order.
|
|
249
269
|
*/
|
|
250
|
-
async function waitForTranscripts(
|
|
270
|
+
async function waitForTranscripts(
|
|
271
|
+
jobs: TranscriptJob[],
|
|
272
|
+
ctx: WorkflowCtx,
|
|
273
|
+
): Promise<TranscriptState[]> {
|
|
251
274
|
let pending = jobs;
|
|
252
275
|
// Keyed by episode id rather than appended, and that is what keeps the digest
|
|
253
276
|
// in PUBLICATION order — see this function's doc.
|
|
254
277
|
const settled = new Map<string, TranscriptState>();
|
|
255
278
|
|
|
256
279
|
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS && pending.length > 0; attempt += 1) {
|
|
257
|
-
const polled = await mapConcurrent(pending, POLL_CONCURRENCY,
|
|
280
|
+
const polled = await mapConcurrent(pending, POLL_CONCURRENCY, (job) =>
|
|
281
|
+
ctx.step("pollTranscript", () => pollTranscript(job), { maxAttempts: 5 }),
|
|
282
|
+
);
|
|
258
283
|
for (const state of polled) {
|
|
259
284
|
if (state.transcriptStatus !== "submitted") settled.set(state.id, state);
|
|
260
285
|
}
|
|
261
286
|
pending = polled.filter((state) => state.transcriptStatus === "submitted");
|
|
262
|
-
if (pending.length > 0) await sleep(
|
|
287
|
+
if (pending.length > 0) await ctx.sleep(POLL_DELAY_MS);
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
for (const job of pending) settled.set(job.id, gaveUpOn(job));
|
|
@@ -305,8 +330,6 @@ function gaveUpOn(job: TranscriptJob): TranscriptState {
|
|
|
305
330
|
* one-second default instead.
|
|
306
331
|
*/
|
|
307
332
|
export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
|
|
308
|
-
"use step";
|
|
309
|
-
|
|
310
333
|
await report(`Submitting ${episode.title} for transcription.`);
|
|
311
334
|
try {
|
|
312
335
|
const { id } = await stepTranscribeSubmitClassified(episode.audioUrl, {
|
|
@@ -320,8 +343,6 @@ export async function submitTranscript(episode: Episode): Promise<TranscriptJob>
|
|
|
320
343
|
}
|
|
321
344
|
}
|
|
322
345
|
|
|
323
|
-
submitTranscript.maxRetries = 4;
|
|
324
|
-
|
|
325
346
|
/**
|
|
326
347
|
* Ask once whether one job has finished.
|
|
327
348
|
*
|
|
@@ -332,8 +353,6 @@ submitTranscript.maxRetries = 4;
|
|
|
332
353
|
* for "still going".
|
|
333
354
|
*/
|
|
334
355
|
export async function pollTranscript(job: TranscriptJob): Promise<TranscriptState | TranscriptJob> {
|
|
335
|
-
"use step";
|
|
336
|
-
|
|
337
356
|
if (job.transcriptStatus === "unavailable") return job;
|
|
338
357
|
|
|
339
358
|
try {
|
|
@@ -358,12 +377,8 @@ export async function pollTranscript(job: TranscriptJob): Promise<TranscriptStat
|
|
|
358
377
|
}
|
|
359
378
|
}
|
|
360
379
|
|
|
361
|
-
pollTranscript.maxRetries = 4;
|
|
362
|
-
|
|
363
380
|
/** Reduce one transcript to the summary and points the digest carries. */
|
|
364
381
|
export async function summarizeTranscript(state: TranscriptState): Promise<EpisodeDigest> {
|
|
365
|
-
"use step";
|
|
366
|
-
|
|
367
382
|
if (state.transcriptStatus === "unavailable") {
|
|
368
383
|
// Still an entry in the digest. A reader who sees four summaries and one
|
|
369
384
|
// stated reason knows what happened; four summaries and silence looks like
|
|
@@ -404,17 +419,21 @@ export async function summarizeTranscript(state: TranscriptState): Promise<Episo
|
|
|
404
419
|
};
|
|
405
420
|
}
|
|
406
421
|
|
|
407
|
-
summarizeTranscript.maxRetries = 5;
|
|
408
|
-
|
|
409
422
|
/**
|
|
410
423
|
* The clock, as a step.
|
|
411
424
|
*
|
|
412
425
|
* A step's result is journaled and therefore stable across replays, where
|
|
413
426
|
* `new Date()` in the body would answer differently on every one — and a body
|
|
414
|
-
* that is not deterministic is a body the
|
|
427
|
+
* that is not deterministic is a body the engine cannot replay.
|
|
428
|
+
*
|
|
429
|
+
* The read below is therefore a BASELINED occurrence of `guard-invariants`
|
|
430
|
+
* rule 30, and that is the reason: it is inside a step, not inside a body. The
|
|
431
|
+
* rule bans the call anywhere in a shipped `workflows/` file because the
|
|
432
|
+
* `ctx.step` callback boundary is not decidable from a line; `dailyDigestFlow`
|
|
433
|
+
* is what reaches this one, as `ctx.step("timestamp", () => timestamp())`.
|
|
434
|
+
* Anything at BODY level is the bug, not an exception.
|
|
415
435
|
*/
|
|
416
436
|
export async function timestamp(): Promise<string> {
|
|
417
|
-
"use step";
|
|
418
437
|
return new Date().toISOString();
|
|
419
438
|
}
|
|
420
439
|
|
|
@@ -45,9 +45,8 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import { report } from "@alexkroman1/aai/step";
|
|
48
|
-
import { stepFetchOk } from "@alexkroman1/aai/step-errors";
|
|
48
|
+
import { FatalError, stepFetchOk } from "@alexkroman1/aai/step-errors";
|
|
49
49
|
import { decodeHtmlEntities, isRecord, omitUndefined, safeJsonParse } from "@alexkroman1/aai/utils";
|
|
50
|
-
import { FatalError } from "workflow";
|
|
51
50
|
|
|
52
51
|
/** How long any one of these lookups may take before it is a failure. */
|
|
53
52
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -93,8 +92,6 @@ export async function discoverEpisodes(
|
|
|
93
92
|
podcastChannels: string,
|
|
94
93
|
maxEpisodes: number,
|
|
95
94
|
): Promise<Episode[]> {
|
|
96
|
-
"use step";
|
|
97
|
-
|
|
98
95
|
await report("Finding recent podcast episodes.");
|
|
99
96
|
const links = parsePodcastChannels(podcastChannels);
|
|
100
97
|
if (links.length === 0) throw new FatalError("Add at least one podcast link.");
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
*
|
|
20
20
|
* ## What a step still owns
|
|
21
21
|
*
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* The step BOUNDARY is not here — only a body holds a `ctx`, and the call this
|
|
23
|
+
* file is reached through is `ctx.step("postDigest", …)` in `digest.ts`. What
|
|
24
|
+
* stays here is what a step DOES: the digest rendered as a `ChannelMessage` and
|
|
25
|
+
* one `sendToChannelClassified` call. Deciding which steps exist is the
|
|
26
|
+
* template's job; what happens inside one is the SDK's.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
29
|
import { type ChannelMessage, slackChannel } from "@alexkroman1/aai/channels";
|
|
@@ -51,8 +52,6 @@ export type SlackDigestInput = {
|
|
|
51
52
|
* `RetryableError` carrying Slack's own `Retry-After` on a 5xx.
|
|
52
53
|
*/
|
|
53
54
|
export async function sendDigestToSlack(input: SlackDigestInput): Promise<string> {
|
|
54
|
-
"use step";
|
|
55
|
-
|
|
56
55
|
await report("Posting the digest to Slack.");
|
|
57
56
|
return await sendToChannelClassified(
|
|
58
57
|
slackChannel({ webhookUrl: input.slackWebhookUrl, textParam: input.slackWorkflowTextParam }),
|
|
@@ -16,12 +16,33 @@
|
|
|
16
16
|
// `sleep()` is RECORDED rather than taken — so the poll loop below runs at
|
|
17
17
|
// full speed and the only way to observe a run in flight is to hold the
|
|
18
18
|
// provider's answer, which is what `hold` does.
|
|
19
|
-
// *
|
|
20
|
-
// template's headline port of Temporal's `expense` —
|
|
21
|
-
//
|
|
22
|
-
// `
|
|
23
|
-
//
|
|
24
|
-
//
|
|
19
|
+
// * **Nobody can answer a hook here**, which reaches the RETENTION GATE — this
|
|
20
|
+
// template's headline port of Temporal's `expense` — through exactly ONE of
|
|
21
|
+
// its three branches. `ctx.waitFor` carrying a `timeoutMs` resolves
|
|
22
|
+
// `undefined` in this engine, which IS the closed window, so every run that
|
|
23
|
+
// gets past `summarize` takes the gate's safe default: it deletes the
|
|
24
|
+
// transcript it made and completes. That branch is asserted below. The two
|
|
25
|
+
// ANSWERED branches are the unreachable ones — `signal()` answers `false`
|
|
26
|
+
// for every token, there being nothing in process to deliver a payload — so
|
|
27
|
+
// an approval, a decline and `keep_transcript` itself are `agent.test.ts`'s
|
|
28
|
+
// to drive against `askWhetherToKeep`, and it is the only tier that can.
|
|
29
|
+
// * **Every run here takes the NO-CALLBACK arm, and that must not be read as
|
|
30
|
+
// coverage of the webhook path.** The eval publishes no webhook minter, so
|
|
31
|
+
// `stepWebhookUrl` throws, the template's `callbackUrl` degrades to
|
|
32
|
+
// `undefined`, the job is submitted with no `webhook_url`, and
|
|
33
|
+
// `awaitTranscript` never parks on a hook at all — it polls, exactly as it
|
|
34
|
+
// did before the provider could call back. That is worth having rather than
|
|
35
|
+
// working around: it is the arm a deployment with no public URL takes, it is
|
|
36
|
+
// the arm a dropped delivery lands on, and it is the one that must never
|
|
37
|
+
// hang. A case below asserts it POSITIVELY — that the submitted job carries
|
|
38
|
+
// no `webhook_url` — so an eval runtime that later publishes a minter fails
|
|
39
|
+
// here instead of silently changing what all of these cases measure.
|
|
40
|
+
//
|
|
41
|
+
// What no eval here can show is a delivery RESUMING a run, so no case claims
|
|
42
|
+
// it. The answered arm is `agent.test.ts`'s, through
|
|
43
|
+
// `createWorkflowCtx({ hooks })`, which is the only tier that can send a
|
|
44
|
+
// payload at all; a real HTTP POST to the public callback route is
|
|
45
|
+
// `aai-cli`'s `dev-workflow.scenario.test.ts`'s, and is not yet written.
|
|
25
46
|
// * **The provider is scripted, in BOTH modes**, through `stepFetch`'s
|
|
26
47
|
// published slot — so the transcription, the recap's model call and the
|
|
27
48
|
// compensating DELETE are all deterministic and free, while the SESSION's
|
|
@@ -186,7 +207,10 @@ const START_TURN = [
|
|
|
186
207
|
* Not tidiness: the scripted provider is unpublished when the test that
|
|
187
208
|
* installed it finishes, so a body still mid-flight would make its next request
|
|
188
209
|
* against whatever the next case publishes — or against the real provider, with
|
|
189
|
-
* a real key.
|
|
210
|
+
* a real key. A run drained here COMPLETES rather than failing, and the last
|
|
211
|
+
* thing it does on the way is delete its own transcript: the retention gate's
|
|
212
|
+
* window closes with nobody having answered, which is the safe default. See the
|
|
213
|
+
* header, and the case that pins it.
|
|
190
214
|
*/
|
|
191
215
|
async function drain(workflows: EvalWorkflows | undefined, provider: ScriptedProvider) {
|
|
192
216
|
provider.release();
|
|
@@ -226,10 +250,18 @@ describeEval(
|
|
|
226
250
|
expect(posts).toHaveLength(1);
|
|
227
251
|
// And it submitted the recording the DESK supplies, because a phone
|
|
228
252
|
// caller cannot read a URL aloud.
|
|
229
|
-
|
|
253
|
+
const submitted = JSON.parse(String(posts[0]?.body));
|
|
254
|
+
expect(submitted).toMatchObject({
|
|
230
255
|
audio_url: SAMPLE_RECORDING,
|
|
231
256
|
speaker_labels: true,
|
|
232
257
|
});
|
|
258
|
+
// NO `webhook_url`, asserted positively — see the third boundary in this
|
|
259
|
+
// file's header. Nothing publishes a webhook minter here, so
|
|
260
|
+
// `stepWebhookUrl` throws, `callbackUrl` degrades, and every run in this
|
|
261
|
+
// file polls rather than parking on a callback. Pinning it here is what
|
|
262
|
+
// stops an eval runtime that later publishes one from silently changing
|
|
263
|
+
// which arm all of these cases measure.
|
|
264
|
+
expect(Object.keys(submitted)).not.toContain("webhook_url");
|
|
233
265
|
|
|
234
266
|
await drain(workflows, provider);
|
|
235
267
|
},
|
|
@@ -328,6 +360,38 @@ describeEval(
|
|
|
328
360
|
},
|
|
329
361
|
);
|
|
330
362
|
|
|
363
|
+
test("nobody answers the retention gate, so the transcript is not kept", async ({
|
|
364
|
+
workflows,
|
|
365
|
+
}) => {
|
|
366
|
+
// The gate's SAFE DEFAULT, and the branch of it this tier really does
|
|
367
|
+
// reach: `ctx.waitFor` here carries a `timeoutMs` and no one can send a
|
|
368
|
+
// payload, which is the closed window rather than a missing feature. So
|
|
369
|
+
// this is the ordinary ending of every run in this file, and it is worth
|
|
370
|
+
// an assertion of its own — a gate whose no-answer branch KEPT the data
|
|
371
|
+
// would be a prompt with a grace period, and nothing else here would
|
|
372
|
+
// notice the difference.
|
|
373
|
+
const provider = stubProvider();
|
|
374
|
+
|
|
375
|
+
const run = await workflows?.run(recap, {
|
|
376
|
+
url: SAMPLE_RECORDING,
|
|
377
|
+
requestedBy: "eval-session",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(run?.status).toBe("completed");
|
|
381
|
+
// `answered: false` is the half that separates this from a caller who
|
|
382
|
+
// said no: the desk reports which of the two happened rather than only
|
|
383
|
+
// what it did.
|
|
384
|
+
expect(run?.output).toMatchObject({ kept: false, answered: false });
|
|
385
|
+
// And the default really is DELETE — the same request the compensation
|
|
386
|
+
// makes, reached by the opposite path: this run succeeded.
|
|
387
|
+
expect(requests(provider, "DELETE").map((one) => one.url)).toEqual([
|
|
388
|
+
`https://api.assemblyai.com/v2/transcript/${TRANSCRIPT_ID}`,
|
|
389
|
+
]);
|
|
390
|
+
// The caller was ASKED first, which is what makes two minutes of silence
|
|
391
|
+
// an answer at all.
|
|
392
|
+
expect(run?.reported.join("\n")).toMatch(/Keep the transcript on file/);
|
|
393
|
+
});
|
|
394
|
+
|
|
331
395
|
test("a run that fails after creating a transcript deletes it again", async ({ workflows }) => {
|
|
332
396
|
// Started from the CASE rather than through a tool, because the subject
|
|
333
397
|
// is the saga and the failure has to be injected: the provider refuses
|