@alexkroman1/aai-cli 6.11.0 → 8.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/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* The flow: watch some podcasts, transcribe what is new, summarize it, and post
|
|
4
|
+
* a digest to Slack — then do it again on a schedule, for as long as asked.
|
|
5
|
+
*
|
|
6
|
+
* ```text
|
|
7
|
+
* discoverEpisodes one step — links in, episodes with audio out
|
|
8
|
+
* submitTranscript N steps — hand each episode to AssemblyAI
|
|
9
|
+
* waitForTranscripts the BODY — poll the whole batch, sleeping between rounds
|
|
10
|
+
* summarizeTranscript N steps — one model call per finished transcript
|
|
11
|
+
* sendDigestToSlack one step — the message
|
|
12
|
+
* sleep(interval) the BODY — and around again
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ## What this template is FOR
|
|
16
|
+
*
|
|
17
|
+
* The other workflow templates are one-shot: something arrives, a run processes
|
|
18
|
+
* it, the run ends. This one is the shape nothing else here demonstrates — a
|
|
19
|
+
* run that is **long-lived and periodic**. It sleeps for days at a time and
|
|
20
|
+
* wakes up to do the same work again, which is only possible because a durable
|
|
21
|
+
* sleep is suspension rather than waiting: nothing is resident, nothing is
|
|
22
|
+
* billed, and the run survives the agent restarting, redeploying or going idle
|
|
23
|
+
* underneath it.
|
|
24
|
+
*
|
|
25
|
+
* That single fact is why `daysToRun` exists as an input at all. A run that
|
|
26
|
+
* repeats forever is a resource nobody can see and nobody remembers to cancel,
|
|
27
|
+
* so this one is asked up front how many digests it owes and then finishes.
|
|
28
|
+
*
|
|
29
|
+
* ## Storage is not optional here, unlike everywhere else
|
|
30
|
+
*
|
|
31
|
+
* Every other template says "enable the database for durability, but it is fine
|
|
32
|
+
* to build without it". For this one it is closer to a requirement: without
|
|
33
|
+
* storage the run lives in the process, and a process does not survive a
|
|
34
|
+
* multi-day sleep. Set `DATABASE_URL` under `aai dev`, or `aai storage enable`
|
|
35
|
+
* once deployed. Build it with `intervalUnit: "minutes"` and you will see it
|
|
36
|
+
* work either way; leave it on `days` without storage and the second digest
|
|
37
|
+
* never arrives.
|
|
38
|
+
*
|
|
39
|
+
* ## Batch polling, which is this file's one genuinely new mechanism
|
|
40
|
+
*
|
|
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
|
|
43
|
+
* episodes are in flight at once and they finish out of order, so the loop has
|
|
44
|
+
* to carry a shrinking pending set and let the finished ones drop out — see
|
|
45
|
+
* {@link waitForTranscripts}. It is the same idea one dimension up, and the
|
|
46
|
+
* reason it stays in the template rather than moving to the SDK is that the SDK
|
|
47
|
+
* owns what is INSIDE a step and never the body's control flow.
|
|
48
|
+
*
|
|
49
|
+
* @module digest
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { mapConcurrent, report, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
53
|
+
import {
|
|
54
|
+
stepGenerateJsonClassified,
|
|
55
|
+
stepTranscribePollClassified,
|
|
56
|
+
stepTranscribeSubmitClassified,
|
|
57
|
+
} from "@alexkroman1/aai/step-errors";
|
|
58
|
+
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
59
|
+
import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
|
|
60
|
+
import { FatalError, sleep } from "workflow";
|
|
61
|
+
import { z } from "zod";
|
|
62
|
+
import type { dailyDigest } from "../agent.ts";
|
|
63
|
+
import { discoverEpisodes, type Episode } from "./feeds.ts";
|
|
64
|
+
import { sendDigestToSlack } from "./slack.ts";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Between polling rounds. Transcription is minutes, so this is not a busy wait.
|
|
68
|
+
*
|
|
69
|
+
* Exported so `agent.eval.test.ts` can assert on the wait a run RECORDED without
|
|
70
|
+
* restating the number — an eval skips the sleep, so the duration asked for is
|
|
71
|
+
* the only observable there is.
|
|
72
|
+
*/
|
|
73
|
+
export const POLL_DELAY = "20 seconds";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 180 rounds x 20s = an hour, which is far past any podcast episode.
|
|
77
|
+
*
|
|
78
|
+
* Exported for the same reason as {@link POLL_DELAY}: the eval drives a job past
|
|
79
|
+
* this budget and reads the degraded entry, and a restated 180 in two files is a
|
|
80
|
+
* second place for the number to be wrong.
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_POLL_ATTEMPTS = 180;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* How much transcript the model reads.
|
|
86
|
+
*
|
|
87
|
+
* A three-hour episode is far more than a summary needs and more than the
|
|
88
|
+
* context window wants to pay for. Truncating is the right call for a DIGEST
|
|
89
|
+
* specifically: the opening of an episode is where its subject is stated.
|
|
90
|
+
*/
|
|
91
|
+
const MAX_TRANSCRIPT_CHARS = 18_000;
|
|
92
|
+
|
|
93
|
+
/** Submissions in flight at once — polite to the provider, still parallel. */
|
|
94
|
+
const SUBMIT_CONCURRENCY = 2;
|
|
95
|
+
|
|
96
|
+
/** Status checks in flight at once. Cheaper calls, so a wider gate. */
|
|
97
|
+
const POLL_CONCURRENCY = 3;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What a run is started with — the schema's OUTPUT, so every `.default()` has
|
|
101
|
+
* already run and nothing here is optional.
|
|
102
|
+
*
|
|
103
|
+
* Derived rather than restated. The hand-written version this replaces declared
|
|
104
|
+
* six optional fields against a schema where all six carry a default, so the
|
|
105
|
+
* body re-applied every one of them with `??` and a `.default(5)` beside a
|
|
106
|
+
* `?? 3` could disagree with nothing reporting it.
|
|
107
|
+
*/
|
|
108
|
+
export type DigestInput = WorkflowInputOf<typeof dailyDigest>;
|
|
109
|
+
|
|
110
|
+
export type IntervalUnit = "minutes" | "hours" | "days";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* An episode on its way through transcription.
|
|
114
|
+
*
|
|
115
|
+
* A discriminated union rather than an optional `transcript` field, so the
|
|
116
|
+
* "we could not transcribe this one" case is a STATE the compiler makes every
|
|
117
|
+
* reader handle — not a `undefined` somebody forgets to check. One bad episode
|
|
118
|
+
* must not sink a digest of five.
|
|
119
|
+
*/
|
|
120
|
+
type TranscriptJob = Episode &
|
|
121
|
+
(
|
|
122
|
+
| { transcriptStatus: "submitted"; transcriptId: string }
|
|
123
|
+
| { transcriptStatus: "unavailable"; reason: string }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
type TranscriptState = Episode &
|
|
127
|
+
(
|
|
128
|
+
| { transcriptStatus: "done"; transcriptId: string; transcript: string; durationMs: number }
|
|
129
|
+
| { transcriptStatus: "unavailable"; reason: string }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/** One episode as it appears in the message and on the page. */
|
|
133
|
+
export type EpisodeDigest = Episode & {
|
|
134
|
+
transcriptSource: "assemblyai" | "unavailable";
|
|
135
|
+
summary: string;
|
|
136
|
+
keyPoints: string[];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type DailyDigestOutput = {
|
|
140
|
+
podcastChannels: string;
|
|
141
|
+
deliveryTarget: string;
|
|
142
|
+
scheduleInterval: string;
|
|
143
|
+
digestsScheduled: number;
|
|
144
|
+
digestsSent: number;
|
|
145
|
+
lastDigest: {
|
|
146
|
+
sentAt: string;
|
|
147
|
+
slackStatus: string;
|
|
148
|
+
episodes: EpisodeDigest[];
|
|
149
|
+
} | null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** What the model must answer with, and what `stepGenerateJsonClassified` enforces. */
|
|
153
|
+
const SummaryReply = z.object({
|
|
154
|
+
summary: z.string().trim().min(1),
|
|
155
|
+
keyPoints: z.array(z.string().trim().min(1)).min(1).max(5),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The body.
|
|
160
|
+
*
|
|
161
|
+
* Everything it does is a step call or a `sleep`, which is what makes it legal
|
|
162
|
+
* to replay: the DevKit re-runs this function from the top after any crash, and
|
|
163
|
+
* each step it reaches is either replayed from the journal or executed for the
|
|
164
|
+
* first time. Nothing here reads a clock, generates an id, or touches anything
|
|
165
|
+
* that would answer differently on the second pass — {@link timestamp} is a step
|
|
166
|
+
* for exactly that reason.
|
|
167
|
+
*/
|
|
168
|
+
export async function dailyDigestFlow(input: DigestInput): Promise<DailyDigestOutput> {
|
|
169
|
+
"use workflow";
|
|
170
|
+
|
|
171
|
+
// No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
|
|
172
|
+
// `.default()` has already run by the time a run reaches this line. The
|
|
173
|
+
// chain this replaces restated all four of them, which is a second place for
|
|
174
|
+
// the number to be wrong.
|
|
175
|
+
const { daysToRun: totalDigests, maxEpisodesPerDigest: maxEpisodes } = input;
|
|
176
|
+
const { intervalEvery, intervalUnit } = input;
|
|
177
|
+
const intervalMs = scheduleIntervalMs(intervalEvery, intervalUnit);
|
|
178
|
+
|
|
179
|
+
let lastDigest: DailyDigestOutput["lastDigest"] = null;
|
|
180
|
+
let digestsSent = 0;
|
|
181
|
+
|
|
182
|
+
for (let digestNumber = 1; digestNumber <= totalDigests; digestNumber += 1) {
|
|
183
|
+
const episodes = await discoverEpisodes(input.podcastChannels, maxEpisodes);
|
|
184
|
+
const jobs = await mapConcurrent(episodes, SUBMIT_CONCURRENCY, submitTranscript);
|
|
185
|
+
const transcripts = await waitForTranscripts(jobs);
|
|
186
|
+
const digests = await mapConcurrent(transcripts, SUBMIT_CONCURRENCY, summarizeTranscript);
|
|
187
|
+
|
|
188
|
+
const slackStatus = await sendDigestToSlack({
|
|
189
|
+
slackWebhookUrl: input.slackWebhookUrl,
|
|
190
|
+
slackWorkflowTextParam: input.slackWorkflowTextParam,
|
|
191
|
+
podcastChannels: input.podcastChannels,
|
|
192
|
+
episodes: digests,
|
|
193
|
+
digestNumber,
|
|
194
|
+
totalDigests,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
lastDigest = { sentAt: await timestamp(), slackStatus, episodes: digests };
|
|
198
|
+
digestsSent += 1;
|
|
199
|
+
|
|
200
|
+
// Not after the last one: a run that has delivered everything it owes
|
|
201
|
+
// should end, not sleep for a day and then end.
|
|
202
|
+
if (digestNumber < totalDigests) await sleep(intervalMs);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
podcastChannels: input.podcastChannels,
|
|
207
|
+
deliveryTarget: "Slack webhook",
|
|
208
|
+
scheduleInterval: formatScheduleInterval(intervalEvery, intervalUnit),
|
|
209
|
+
digestsScheduled: totalDigests,
|
|
210
|
+
digestsSent,
|
|
211
|
+
lastDigest,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Wait for a whole BATCH of transcripts, letting them finish out of order.
|
|
217
|
+
*
|
|
218
|
+
* A plain async function rather than a step, and not because it is small: it
|
|
219
|
+
* calls steps and it `sleep`s, neither of which a step may do. So it runs as
|
|
220
|
+
* part of the body and is replayed with it — legal for the ordinary reason,
|
|
221
|
+
* that every line is either a step call or a `sleep`.
|
|
222
|
+
*
|
|
223
|
+
* The shape to notice is that `pending` SHRINKS. A loop that waited for all N
|
|
224
|
+
* on every round would hold the whole batch hostage to its slowest member, and
|
|
225
|
+
* with `maxEpisodesPerDigest` up to 20 that is the difference between a digest
|
|
226
|
+
* arriving and a digest timing out. Finished episodes move to `completed` and
|
|
227
|
+
* are never polled again.
|
|
228
|
+
*
|
|
229
|
+
* Running out of rounds is NOT an error. An episode nobody could transcribe in
|
|
230
|
+
* an hour becomes `unavailable` with a reason, and the digest goes out with the
|
|
231
|
+
* other four — a partial digest being obviously better than none, and the
|
|
232
|
+
* reason being printed where a reader will see it.
|
|
233
|
+
*
|
|
234
|
+
* **What comes back is in the order it went in**, which is not what a first
|
|
235
|
+
* draft does. Appending each episode as it finishes — and concatenating the
|
|
236
|
+
* ones that never did on the end — makes the digest's running order a report of
|
|
237
|
+
* TRANSCRIPTION LATENCY: `discoverEpisodes` sorted the feed newest first, and a
|
|
238
|
+
* reader then sees whichever episode the provider happened to finish first at
|
|
239
|
+
* the top. Found by `agent.eval.test.ts`, which is the only tier that can see
|
|
240
|
+
* it, a per-step spec having no batch to order.
|
|
241
|
+
*/
|
|
242
|
+
async function waitForTranscripts(jobs: TranscriptJob[]): Promise<TranscriptState[]> {
|
|
243
|
+
let pending = jobs;
|
|
244
|
+
// Keyed by episode id rather than appended, and that is what keeps the digest
|
|
245
|
+
// in PUBLICATION order — see this function's doc.
|
|
246
|
+
const settled = new Map<string, TranscriptState>();
|
|
247
|
+
|
|
248
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS && pending.length > 0; attempt += 1) {
|
|
249
|
+
const polled = await mapConcurrent(pending, POLL_CONCURRENCY, pollTranscript);
|
|
250
|
+
for (const state of polled) {
|
|
251
|
+
if (state.transcriptStatus !== "submitted") settled.set(state.id, state);
|
|
252
|
+
}
|
|
253
|
+
pending = polled.filter((state) => state.transcriptStatus === "submitted");
|
|
254
|
+
if (pending.length > 0) await sleep(POLL_DELAY);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const job of pending) settled.set(job.id, gaveUpOn(job));
|
|
258
|
+
// The list `jobs` arrived in, which `discoverEpisodes` sorted newest first.
|
|
259
|
+
return jobs.map((job) => settled.get(job.id) ?? gaveUpOn(job));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One episode the run is done waiting for.
|
|
264
|
+
*
|
|
265
|
+
* Its own function because {@link waitForTranscripts} needs it in two places —
|
|
266
|
+
* the jobs still pending when the budget ran out, and the unreachable fallback
|
|
267
|
+
* the ordered rebuild above needs for a lookup that cannot miss.
|
|
268
|
+
*/
|
|
269
|
+
function gaveUpOn(job: TranscriptJob): TranscriptState {
|
|
270
|
+
return {
|
|
271
|
+
...job,
|
|
272
|
+
transcriptStatus: "unavailable" as const,
|
|
273
|
+
reason:
|
|
274
|
+
job.transcriptStatus === "submitted"
|
|
275
|
+
? // The transcript is not lost — it is still on the provider, and the
|
|
276
|
+
// message says where, the same way the other transcription templates do.
|
|
277
|
+
`Transcript ${job.transcriptId} was still unfinished after ${MAX_POLL_ATTEMPTS} ` +
|
|
278
|
+
`checks. It is not lost — read it with GET ${TRANSCRIBE_API}/v2/transcript/${job.transcriptId}.`
|
|
279
|
+
: job.reason,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Hand one episode's audio to AssemblyAI.
|
|
285
|
+
*
|
|
286
|
+
* The `catch` is the interesting line, and the whole partial-failure policy is
|
|
287
|
+
* in it: transport problems retry, this-episode-is-broken problems degrade. A
|
|
288
|
+
* terminal failure — a 404 on the media URL, a file that is not audio — becomes
|
|
289
|
+
* an `unavailable` VALUE rather than a throw, because one bad episode must not
|
|
290
|
+
* sink a digest of five.
|
|
291
|
+
*
|
|
292
|
+
* The verdict itself is the SDK's: `stepTranscribeSubmitClassified` reads
|
|
293
|
+
* `TranscribeError`'s own `retryable` AND its `retryAfter`, and throws a
|
|
294
|
+
* `FatalError` or a `RetryableError` accordingly. The hand-written
|
|
295
|
+
* `err instanceof TranscribeError && err.retryable` this replaces read only the
|
|
296
|
+
* first of those, so a provider that named a delay was retried on the DevKit's
|
|
297
|
+
* one-second default instead.
|
|
298
|
+
*/
|
|
299
|
+
export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
|
|
300
|
+
"use step";
|
|
301
|
+
|
|
302
|
+
await report(`Submitting ${episode.title} for transcription.`);
|
|
303
|
+
try {
|
|
304
|
+
const { id } = await stepTranscribeSubmitClassified(episode.audioUrl, {
|
|
305
|
+
// A digest quotes nobody, so who spoke costs time for nothing.
|
|
306
|
+
params: { speaker_labels: false },
|
|
307
|
+
});
|
|
308
|
+
return { ...episode, transcriptStatus: "submitted", transcriptId: id };
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (!FatalError.is(err)) throw err;
|
|
311
|
+
return { ...episode, transcriptStatus: "unavailable", reason: errorMessage(err) };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
submitTranscript.maxRetries = 4;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Ask once whether one job has finished.
|
|
319
|
+
*
|
|
320
|
+
* One poll is one step, so each round is journaled on its own: a run that dies
|
|
321
|
+
* mid-wait resumes knowing the last answer instead of re-transcribing. It
|
|
322
|
+
* returns the job UNCHANGED when the answer is "not yet", which is what lets
|
|
323
|
+
* {@link waitForTranscripts} keep it in `pending` without a second vocabulary
|
|
324
|
+
* for "still going".
|
|
325
|
+
*/
|
|
326
|
+
export async function pollTranscript(job: TranscriptJob): Promise<TranscriptState | TranscriptJob> {
|
|
327
|
+
"use step";
|
|
328
|
+
|
|
329
|
+
if (job.transcriptStatus === "unavailable") return job;
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const progress = await stepTranscribePollClassified(job.transcriptId);
|
|
333
|
+
// Branch on `done`, never on a status string: a vocabulary this body does
|
|
334
|
+
// not own would otherwise read as "not finished yet" forever.
|
|
335
|
+
if (!progress.done) return job;
|
|
336
|
+
|
|
337
|
+
await report(`Transcribed ${job.title}.`);
|
|
338
|
+
return {
|
|
339
|
+
...job,
|
|
340
|
+
transcriptStatus: "done",
|
|
341
|
+
transcriptId: job.transcriptId,
|
|
342
|
+
transcript: progress.transcript.text,
|
|
343
|
+
durationMs: progress.transcript.durationMs,
|
|
344
|
+
};
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// Same policy as {@link submitTranscript}: the SDK classified it, a
|
|
347
|
+
// retryable verdict goes back to the DevKit, a terminal one degrades.
|
|
348
|
+
if (!FatalError.is(err)) throw err;
|
|
349
|
+
return { ...job, transcriptStatus: "unavailable", reason: errorMessage(err) };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
pollTranscript.maxRetries = 4;
|
|
354
|
+
|
|
355
|
+
/** Reduce one transcript to the summary and points the digest carries. */
|
|
356
|
+
export async function summarizeTranscript(state: TranscriptState): Promise<EpisodeDigest> {
|
|
357
|
+
"use step";
|
|
358
|
+
|
|
359
|
+
if (state.transcriptStatus === "unavailable") {
|
|
360
|
+
// Still an entry in the digest. A reader who sees four summaries and one
|
|
361
|
+
// stated reason knows what happened; four summaries and silence looks like
|
|
362
|
+
// the feed simply had four episodes.
|
|
363
|
+
return {
|
|
364
|
+
...episodeOf(state),
|
|
365
|
+
transcriptSource: "unavailable",
|
|
366
|
+
summary: `This episode could not be transcribed: ${state.reason}`,
|
|
367
|
+
keyPoints: ["No transcript was available to summarize."],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await report(`Summarizing ${state.title}.`);
|
|
372
|
+
const parsed = await stepGenerateJsonClassified(
|
|
373
|
+
[
|
|
374
|
+
`Podcast: ${state.podcastTitle}`,
|
|
375
|
+
`Episode: ${state.title}`,
|
|
376
|
+
`Published: ${state.published}`,
|
|
377
|
+
"",
|
|
378
|
+
"Transcript:",
|
|
379
|
+
state.transcript.slice(0, MAX_TRANSCRIPT_CHARS),
|
|
380
|
+
].join("\n"),
|
|
381
|
+
{
|
|
382
|
+
schema: SummaryReply,
|
|
383
|
+
system:
|
|
384
|
+
"You summarize podcast transcripts for a daily digest. Reply with JSON only: " +
|
|
385
|
+
'{"summary": string, "keyPoints": string[]}. Keep the summary to a few sentences ' +
|
|
386
|
+
"and give 3 to 5 concrete key points — decisions, numbers, names, claims — never " +
|
|
387
|
+
'"the hosts discussed several topics".',
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
...episodeOf(state),
|
|
393
|
+
transcriptSource: "assemblyai",
|
|
394
|
+
summary: parsed.summary,
|
|
395
|
+
keyPoints: parsed.keyPoints,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
summarizeTranscript.maxRetries = 5;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The clock, as a step.
|
|
403
|
+
*
|
|
404
|
+
* A step's result is journaled and therefore stable across replays, where
|
|
405
|
+
* `new Date()` in the body would answer differently on every one — and a body
|
|
406
|
+
* that is not deterministic is a body the DevKit cannot replay.
|
|
407
|
+
*/
|
|
408
|
+
export async function timestamp(): Promise<string> {
|
|
409
|
+
"use step";
|
|
410
|
+
return new Date().toISOString();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---- Pure helpers -----------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
/** The `Episode` half of a state, without its transcript fields. */
|
|
416
|
+
function episodeOf(state: Episode): Episode {
|
|
417
|
+
return {
|
|
418
|
+
id: state.id,
|
|
419
|
+
feedUrl: state.feedUrl,
|
|
420
|
+
podcastTitle: state.podcastTitle,
|
|
421
|
+
title: state.title,
|
|
422
|
+
url: state.url,
|
|
423
|
+
audioUrl: state.audioUrl,
|
|
424
|
+
published: state.published,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** "1 hour", "15 minutes" — the schedule as the page prints it. */
|
|
429
|
+
export function formatScheduleInterval(every: number, unit: IntervalUnit): string {
|
|
430
|
+
return `${every} ${every === 1 ? unit.slice(0, -1) : unit}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The interval in milliseconds, which is one of the three things `sleep` takes.
|
|
435
|
+
*
|
|
436
|
+
* `sleep` also accepts a duration STRING (`"20 seconds"`, as `POLL_DELAY`
|
|
437
|
+
* above), and building one here would look tidier — but that overload is typed
|
|
438
|
+
* as a template-literal union, so a value assembled from two variables does not
|
|
439
|
+
* satisfy it without a cast. A number needs no cast and no trust.
|
|
440
|
+
*/
|
|
441
|
+
export function scheduleIntervalMs(every: number, unit: IntervalUnit): number {
|
|
442
|
+
const multiplier = unit === "minutes" ? 60_000 : unit === "hours" ? 3_600_000 : 86_400_000;
|
|
443
|
+
return every * multiplier;
|
|
444
|
+
}
|