@alexkroman1/aai-cli 5.14.0 → 6.1.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-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* The durable half of the recap desk — and the file the Temporal patterns were
|
|
4
|
+
* ported into.
|
|
5
|
+
*
|
|
6
|
+
* Read `research-workflow/workflows/research.ts` first for the two rules every
|
|
7
|
+
* directive body obeys (replayed from the top, so no live handles and no
|
|
8
|
+
* undurable decisions; step arguments and results cross a queue, so pass an id
|
|
9
|
+
* and not a payload). Both hold here unchanged. What THIS template adds is the
|
|
10
|
+
* three shapes a durable engine exists for, ported from the Temporal TypeScript
|
|
11
|
+
* samples and running against a real provider rather than a stub:
|
|
12
|
+
*
|
|
13
|
+
* | Temporal sample | Ported here as |
|
|
14
|
+
* | --- | --- |
|
|
15
|
+
* | `saga` — `openAccount`'s compensation stack | {@link recapFlow}'s `compensations`, unwound by {@link compensate} |
|
|
16
|
+
* | `polling` — infrequent polling | {@link awaitTranscript}: a bounded loop of one step plus one durable `sleep` |
|
|
17
|
+
* | `timer-examples` — `processOrderWorkflow` | the `Promise.race` against {@link PATIENCE}, then the "still going" note |
|
|
18
|
+
* | `expense` — `timeoutOrUserAction` | the RETENTION GATE: a hook raced against {@link RETENTION_WINDOW}, three outcomes and a safe default |
|
|
19
|
+
*
|
|
20
|
+
* The voice half — start, query, cancel, and the answer to the gate — is ported
|
|
21
|
+
* in `agent.ts`.
|
|
22
|
+
*
|
|
23
|
+
* ## The gate is the one that needed a new SDK primitive
|
|
24
|
+
*
|
|
25
|
+
* Temporal's `expense` sample parks a workflow on a signal until a human
|
|
26
|
+
* approves it, and it is the most voice-native pattern in the whole catalog: the
|
|
27
|
+
* caller IS the approver, and the phone IS the signal channel. It could not be
|
|
28
|
+
* written here at all until `ctx.workflows.signal()` existed — the DevKit's only
|
|
29
|
+
* reachable waitpoint was `createWebhook()`, whose URL is minted for a third
|
|
30
|
+
* party with a callback to make, not for the person already on the line. See
|
|
31
|
+
* that method's doc for the token rules; `workflows/tokens.ts` is this template's
|
|
32
|
+
* one derivation of one.
|
|
33
|
+
*
|
|
34
|
+
* ## Why these are worth porting rather than restating
|
|
35
|
+
*
|
|
36
|
+
* All three are the same observation from different angles: **a `try`/`finally`
|
|
37
|
+
* in a tool body is not a transaction.** The process holding it can die, and
|
|
38
|
+
* everything it was going to clean up is then nobody's. Here every one of those
|
|
39
|
+
* moves is a journaled step:
|
|
40
|
+
*
|
|
41
|
+
* - The compensation stack is unwound by CODE THAT IS ITSELF DURABLE. A crash
|
|
42
|
+
* part-way through the unwind resumes with the already-run compensations
|
|
43
|
+
* replayed from the journal and re-issues only what is left — which is the
|
|
44
|
+
* property a `finally` cannot have, and the reason Temporal's saga sample is
|
|
45
|
+
* the one everybody ports first.
|
|
46
|
+
* - The poll loop's waits are `sleep`, so the sandbox is free to EXIT between
|
|
47
|
+
* attempts. A twenty-minute transcription costs no resident process; a
|
|
48
|
+
* `setInterval` in a tool body would hold one and still lose the work on a
|
|
49
|
+
* redeploy.
|
|
50
|
+
* - The race's timer is journaled too, so "has this been slow?" resolves the
|
|
51
|
+
* same way on a replay as it did live.
|
|
52
|
+
*
|
|
53
|
+
* ## What is real here
|
|
54
|
+
*
|
|
55
|
+
* Everything the desk claims to do. `submitRecording`, `checkTranscript` and
|
|
56
|
+
* `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
|
|
57
|
+
* `DELETE` on `/v2/transcript`), and `summarize` is a real model call through
|
|
58
|
+
* `stepGenerate`. The BATCH API is what makes the polling port honest: it
|
|
59
|
+
* answers with a job id in milliseconds and finishes minutes later, so the wait
|
|
60
|
+
* is the provider's, not a `setTimeout` this template chose. (Its sibling
|
|
61
|
+
* `transcription-workflow` takes the other endpoint — the sync one, which answers in
|
|
62
|
+
* the request and pays for it with a hard cap, so its shape is a fan-out rather
|
|
63
|
+
* than a poll.)
|
|
64
|
+
*
|
|
65
|
+
* A step is handed no `ToolContext`, so the key comes from `requireStepEnv`
|
|
66
|
+
* rather than `ctx.env`; under `aai dev` that means it has to be in `.env`, not
|
|
67
|
+
* just your shell.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import { throwStepError, toStepError } from "@alexkroman1/aai/step-errors";
|
|
71
|
+
import {
|
|
72
|
+
errorMessage,
|
|
73
|
+
isRecord,
|
|
74
|
+
omitUndefined,
|
|
75
|
+
report,
|
|
76
|
+
requireStepEnv,
|
|
77
|
+
stepFetch,
|
|
78
|
+
stepGenerateJson,
|
|
79
|
+
} from "@alexkroman1/aai/utils";
|
|
80
|
+
import { createHook, FatalError, sleep } from "workflow";
|
|
81
|
+
import { z } from "zod";
|
|
82
|
+
import { retentionToken } from "./tokens.ts";
|
|
83
|
+
|
|
84
|
+
/** AssemblyAI's pre-recorded (batch) transcription collection. */
|
|
85
|
+
const TRANSCRIPT_ENDPOINT = "https://api.assemblyai.com/v2/transcript";
|
|
86
|
+
|
|
87
|
+
/** The key a step reads out of the agent env. Declared in `agent.ts`'s `requiredEnv`. */
|
|
88
|
+
const API_KEY_ENV = "ASSEMBLYAI_API_KEY";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* How long the run waits between polls.
|
|
92
|
+
*
|
|
93
|
+
* A `sleep`, so the wait costs nothing: the run is SUSPENDED between attempts
|
|
94
|
+
* and the sandbox may exit. That is what makes the interval a product decision
|
|
95
|
+
* rather than a cost one — fifteen seconds is responsive for a meeting-length
|
|
96
|
+
* file, and the docs' own 1–2 second advice is for a load test with a
|
|
97
|
+
* rate-limit budget to spend.
|
|
98
|
+
*/
|
|
99
|
+
const POLL_INTERVAL = "15 seconds";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Polls before the desk gives up.
|
|
103
|
+
*
|
|
104
|
+
* A bound rather than a deadline, because it is what the LOOP can enforce with
|
|
105
|
+
* nothing but journaled values: attempt N is attempt N on every replay, where a
|
|
106
|
+
* wall-clock deadline read in the body would move under it. At
|
|
107
|
+
* {@link POLL_INTERVAL} this is twenty minutes, which is far past the
|
|
108
|
+
* turnaround of any recording a phone caller will name.
|
|
109
|
+
*/
|
|
110
|
+
const MAX_POLLS = 80;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* How long the desk waits before admitting a recording is a long one.
|
|
114
|
+
*
|
|
115
|
+
* The port of Temporal's `processOrderWorkflow`: race the work against a timer,
|
|
116
|
+
* and if the timer wins, do the other thing — there the delayed-order email,
|
|
117
|
+
* here a progress line the caller hears when they ask. Then keep waiting for
|
|
118
|
+
* the work either way.
|
|
119
|
+
*/
|
|
120
|
+
const PATIENCE = "2 minutes";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* How long the desk holds the transcript waiting for an answer.
|
|
124
|
+
*
|
|
125
|
+
* The port of `timeoutOrUserAction`: Temporal races a `condition()` against a
|
|
126
|
+
* timeout, this races a hook against a `sleep`, and both have THREE outcomes —
|
|
127
|
+
* approved, declined, nobody answered. The window is a `sleep`, so a caller who
|
|
128
|
+
* hangs up costs nothing while it runs.
|
|
129
|
+
*
|
|
130
|
+
* Two minutes because a caller is on the line; a desk whose approver is on email
|
|
131
|
+
* would write `"2 days"` and nothing else in this file would change.
|
|
132
|
+
*/
|
|
133
|
+
const RETENTION_WINDOW = "2 minutes";
|
|
134
|
+
|
|
135
|
+
/** Every HTTP call's deadline. `fetch` has none of its own, and a hung step never ends. */
|
|
136
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Transcript characters handed to the model.
|
|
140
|
+
*
|
|
141
|
+
* The pass-an-id-not-a-payload rule meeting the case where the payload IS the
|
|
142
|
+
* work — the same bound `link-digest` puts on article text, for the same
|
|
143
|
+
* reason: it has to cross the queue, so it is capped rather than trusted.
|
|
144
|
+
*/
|
|
145
|
+
const MAX_TRANSCRIPT_CHARS = 24_000;
|
|
146
|
+
|
|
147
|
+
/** Points a recap is reduced to. */
|
|
148
|
+
const POINTS = 3;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The shape the model must answer in.
|
|
152
|
+
*
|
|
153
|
+
* `stepGenerateJson` validates against this and throws PLAINLY when the reply
|
|
154
|
+
* misses, which is the retry policy in one distinction: a model that answered in
|
|
155
|
+
* prose may answer correctly next time, where a 401 will not. `spoken` is the
|
|
156
|
+
* field this template exists for — without it the announced turn has nothing to
|
|
157
|
+
* read down the phone — so it is required rather than defaulted.
|
|
158
|
+
*/
|
|
159
|
+
const RecapReply = z.object({
|
|
160
|
+
headline: z.string().min(1),
|
|
161
|
+
points: z.array(z.string()).min(1),
|
|
162
|
+
spoken: z.string().min(1),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/** What one finished recap is. Small and JSON-shaped, like every step result. */
|
|
166
|
+
export type Recap = {
|
|
167
|
+
url: string;
|
|
168
|
+
headline: string;
|
|
169
|
+
points: string[];
|
|
170
|
+
/** One sentence a phone call can carry — what the announced turn reads out. */
|
|
171
|
+
spoken: string;
|
|
172
|
+
/** The recording's length, as the provider measured it. */
|
|
173
|
+
minutes: number;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** How the retention gate ended — the three outcomes, named. */
|
|
177
|
+
export type Retention = {
|
|
178
|
+
/** Whether the transcript is still on the account. */
|
|
179
|
+
kept: boolean;
|
|
180
|
+
/** False when the window elapsed with nobody answering. */
|
|
181
|
+
answered: boolean;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** A transcript job, as the provider's own status endpoint reports it. */
|
|
185
|
+
export type TranscriptState = {
|
|
186
|
+
status: "queued" | "processing" | "completed" | "error";
|
|
187
|
+
text?: string;
|
|
188
|
+
error?: string;
|
|
189
|
+
audioDuration?: number;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* One undo, and the name it goes by when it fails.
|
|
194
|
+
*
|
|
195
|
+
* Ported field-for-field from Temporal's `saga` sample, whose `Compensation` is
|
|
196
|
+
* `{ message, fn }`. The label is not decoration: a compensation that fails is
|
|
197
|
+
* SWALLOWED (see {@link compensate}), so the label is the only thing that says
|
|
198
|
+
* what was left behind.
|
|
199
|
+
*/
|
|
200
|
+
export type Compensation = { label: string; undo: () => Promise<void> };
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Transcribe a recording, write it up, and leave nothing behind if it fails.
|
|
204
|
+
*
|
|
205
|
+
* Whatever this returns is what a completed run reports as `output` — which
|
|
206
|
+
* `agent.ts` reads back down the phone, so it is shaped for an ear rather than
|
|
207
|
+
* a page.
|
|
208
|
+
*/
|
|
209
|
+
export async function recapFlow(input: { url: string; requestedBy: string }) {
|
|
210
|
+
"use workflow";
|
|
211
|
+
|
|
212
|
+
// The compensation stack, newest first — `unshift` after each successful
|
|
213
|
+
// acquisition, exactly as Temporal's `openAccount` does. Registering the undo
|
|
214
|
+
// AFTER the step it undoes is the whole discipline: a step that never
|
|
215
|
+
// succeeded has nothing to reverse, and an undo registered before it would
|
|
216
|
+
// reverse a transcript id that does not exist.
|
|
217
|
+
const compensations: Compensation[] = [];
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const job = await submitRecording(input.url);
|
|
221
|
+
compensations.unshift({
|
|
222
|
+
label: `transcript ${job.id}`,
|
|
223
|
+
undo: () => discardTranscript(job.id),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Temporal's `processOrderWorkflow`, line for line: start the work, race it
|
|
227
|
+
// against a timer, and if the timer wins first say so and then go on
|
|
228
|
+
// waiting. `ready` is an ordinary local — deterministic, because the only
|
|
229
|
+
// thing that flips it is a journaled step result.
|
|
230
|
+
let ready = false;
|
|
231
|
+
const work = awaitTranscript(job.id).then((state) => {
|
|
232
|
+
ready = true;
|
|
233
|
+
return state;
|
|
234
|
+
});
|
|
235
|
+
await Promise.race([work, sleep(PATIENCE)]);
|
|
236
|
+
if (!ready) await note("Still transcribing — this is a long one. I'll keep going.");
|
|
237
|
+
const transcript = await work;
|
|
238
|
+
|
|
239
|
+
const recap = await summarize(input.url, transcript);
|
|
240
|
+
const retention = await askWhetherToKeep(input.requestedBy, job.id, compensations);
|
|
241
|
+
return { ...recap, ...retention, requestedBy: input.requestedBy };
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// The saga's whole point. Everything acquired above is released, in reverse,
|
|
244
|
+
// before the failure is re-thrown — and because each undo is a STEP, a crash
|
|
245
|
+
// during the unwind resumes with the finished ones replayed from the journal
|
|
246
|
+
// rather than run twice.
|
|
247
|
+
await compensate(compensations, errorMessage(err));
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Poll one transcript job until it settles.
|
|
254
|
+
*
|
|
255
|
+
* A body-side helper, not a step: it `sleep`s, and a step cannot — a step runs
|
|
256
|
+
* to completion in a worker, where the body is what may suspend. Splitting it
|
|
257
|
+
* out keeps `recapFlow` readable and costs nothing, since the WDK transform
|
|
258
|
+
* rewrites a step's DECLARATION rather than its call sites, so a step called
|
|
259
|
+
* from a helper is still a real step (`mapInBatches` rests on the same
|
|
260
|
+
* property).
|
|
261
|
+
*
|
|
262
|
+
* The loop is deterministic despite looking like it is not: every branch turns
|
|
263
|
+
* on a journaled step result, so a replay takes the same number of turns it took
|
|
264
|
+
* live.
|
|
265
|
+
*/
|
|
266
|
+
export async function awaitTranscript(id: string): Promise<TranscriptState> {
|
|
267
|
+
for (let attempt = 1; attempt <= MAX_POLLS; attempt++) {
|
|
268
|
+
const state = await checkTranscript(id);
|
|
269
|
+
if (state.status === "completed") return state;
|
|
270
|
+
if (state.status === "error") {
|
|
271
|
+
// The provider's own terminal answer. A plain `Error`, not a
|
|
272
|
+
// `FatalError`: this is the BODY, and a body's throw is never retried —
|
|
273
|
+
// `FatalError` is the vocabulary for telling the DevKit not to retry a
|
|
274
|
+
// STEP, and using it here would claim a distinction that does not exist.
|
|
275
|
+
throw new Error(`The provider could not transcribe that recording: ${state.error}`);
|
|
276
|
+
}
|
|
277
|
+
// Suspended, not blocked: nothing is resident while this waits.
|
|
278
|
+
await sleep(POLL_INTERVAL);
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`Gave up on that recording after ${MAX_POLLS} checks.`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Ask the caller whether the transcript stays on file, and act on the answer.
|
|
285
|
+
*
|
|
286
|
+
* The port of Temporal's `timeoutOrUserAction`, and a body-side helper for the
|
|
287
|
+
* same reason {@link awaitTranscript} is: it opens a hook and `sleep`s, neither
|
|
288
|
+
* of which a step can do.
|
|
289
|
+
*
|
|
290
|
+
* The default is DELETE, which is what makes the timeout meaningful. A gate
|
|
291
|
+
* whose no-answer branch keeps the data is not a gate — it is a prompt with a
|
|
292
|
+
* grace period — and for a desk holding transcripts of other people's meetings
|
|
293
|
+
* the safe default is the one that leaves nothing behind.
|
|
294
|
+
*/
|
|
295
|
+
export async function askWhetherToKeep(
|
|
296
|
+
requestedBy: string,
|
|
297
|
+
transcriptId: string,
|
|
298
|
+
compensations: Compensation[],
|
|
299
|
+
): Promise<Retention> {
|
|
300
|
+
// `using`, so the token is released when this scope exits — including the
|
|
301
|
+
// timeout branch, where nothing ever arrives. A hook left registered holds
|
|
302
|
+
// its token against the caller's NEXT run, which `getConflict()` below would
|
|
303
|
+
// then report as a conflict.
|
|
304
|
+
using decision = createHook<{ keep: boolean }>({ token: retentionToken(requestedBy) });
|
|
305
|
+
// Claim the token BEFORE anyone is told to signal it. `createHook()` registers
|
|
306
|
+
// nothing on its own — registration is committed when the workflow suspends —
|
|
307
|
+
// so without this the caller's answer races a token no hook owns yet and is
|
|
308
|
+
// answered "nobody is listening", which is indistinguishable from being late.
|
|
309
|
+
await decision.getConflict();
|
|
310
|
+
|
|
311
|
+
await note(
|
|
312
|
+
`Recap ready. Keep the transcript on file, or delete it? Deleting in ${RETENTION_WINDOW} otherwise.`,
|
|
313
|
+
);
|
|
314
|
+
const answer = await Promise.race([decision, sleep(RETENTION_WINDOW).then(() => undefined)]);
|
|
315
|
+
|
|
316
|
+
if (answer?.keep === true) return { kept: true, answered: true };
|
|
317
|
+
await discardTranscript(transcriptId);
|
|
318
|
+
// Drop the undo now that the run has DONE what it undoes. Leaving it would be
|
|
319
|
+
// harmless (`discardTranscript` treats a 404 as success, as every compensation
|
|
320
|
+
// must) and would still be wrong to read: an unwind that reverses something
|
|
321
|
+
// already gone tells whoever is watching the log a story that did not happen.
|
|
322
|
+
compensations.shift();
|
|
323
|
+
return { kept: false, answered: answer !== undefined };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Run every registered undo, newest first, swallowing failures.
|
|
328
|
+
*
|
|
329
|
+
* Ported from the `compensate` in Temporal's saga sample, including the part
|
|
330
|
+
* that reads like a bug and is not: **a failing compensation must not replace
|
|
331
|
+
* the original error.** The run failed for a reason, the caller needs that
|
|
332
|
+
* reason, and an undo that also failed is a second-order problem — so it is
|
|
333
|
+
* reported and stepped over rather than thrown. The `label` is what makes that
|
|
334
|
+
* report actionable.
|
|
335
|
+
*/
|
|
336
|
+
export async function compensate(compensations: Compensation[], because: string): Promise<void> {
|
|
337
|
+
if (compensations.length === 0) return;
|
|
338
|
+
await note(`Recap failed (${because}) — undoing ${compensations.length} step(s).`);
|
|
339
|
+
for (const compensation of compensations) {
|
|
340
|
+
try {
|
|
341
|
+
await compensation.undo();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
await note(`Could not undo ${compensation.label}: ${errorMessage(err)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---- Steps ------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Hand the recording to the provider.
|
|
352
|
+
*
|
|
353
|
+
* Returns in milliseconds with a job id — the batch API's whole shape, and what
|
|
354
|
+
* makes the poll below a real wait rather than a simulated one.
|
|
355
|
+
*/
|
|
356
|
+
export async function submitRecording(url: string): Promise<{ id: string }> {
|
|
357
|
+
"use step";
|
|
358
|
+
|
|
359
|
+
await report(`Submitting ${new URL(url).hostname} for transcription…`);
|
|
360
|
+
|
|
361
|
+
const response = await request(TRANSCRIPT_ENDPOINT, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
body: JSON.stringify({ audio_url: url, speaker_labels: true }),
|
|
364
|
+
});
|
|
365
|
+
const body = await response.json();
|
|
366
|
+
const id = readString(body, "id");
|
|
367
|
+
if (!id) throw new Error("The provider accepted the recording but named no transcript id.");
|
|
368
|
+
return { id };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Read the job's status once.
|
|
373
|
+
*
|
|
374
|
+
* One poll is one step, so each attempt is journaled on its own: a run that dies
|
|
375
|
+
* mid-wait resumes knowing what the last answer was instead of starting the
|
|
376
|
+
* recording over.
|
|
377
|
+
*/
|
|
378
|
+
export async function checkTranscript(id: string): Promise<TranscriptState> {
|
|
379
|
+
"use step";
|
|
380
|
+
|
|
381
|
+
const response = await request(`${TRANSCRIPT_ENDPOINT}/${id}`);
|
|
382
|
+
const body = await response.json();
|
|
383
|
+
const status = readString(body, "status");
|
|
384
|
+
if (
|
|
385
|
+
status !== "queued" &&
|
|
386
|
+
status !== "processing" &&
|
|
387
|
+
status !== "completed" &&
|
|
388
|
+
status !== "error"
|
|
389
|
+
)
|
|
390
|
+
throw new Error(`The provider reported an unknown transcript status: ${String(status)}`);
|
|
391
|
+
|
|
392
|
+
await report(`Transcript ${status}.`);
|
|
393
|
+
return {
|
|
394
|
+
status,
|
|
395
|
+
// `omitUndefined` rather than a spread-ternary per field: under
|
|
396
|
+
// `exactOptionalPropertyTypes` an absent field and a field set to
|
|
397
|
+
// `undefined` are different types, and this is the SDK's one spelling for
|
|
398
|
+
// the difference.
|
|
399
|
+
...omitUndefined({
|
|
400
|
+
text: readString(body, "text"),
|
|
401
|
+
error: readString(body, "error"),
|
|
402
|
+
audioDuration: readNumber(body, "audio_duration"),
|
|
403
|
+
}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Delete the transcript this run created.
|
|
409
|
+
*
|
|
410
|
+
* The compensation, and a real one: `DELETE /v2/transcript/:id` removes the
|
|
411
|
+
* transcribed text from the account. That matters for a desk that handles call
|
|
412
|
+
* recordings — a run that failed half-way has no business leaving a transcript
|
|
413
|
+
* of somebody's meeting sitting in an account nobody is going to read.
|
|
414
|
+
*
|
|
415
|
+
* A `404` is SUCCESS here, which is the property every compensation needs: the
|
|
416
|
+
* undo has to be safe to run against a world where it already happened, because
|
|
417
|
+
* a replay is exactly that world.
|
|
418
|
+
*/
|
|
419
|
+
export async function discardTranscript(id: string): Promise<void> {
|
|
420
|
+
"use step";
|
|
421
|
+
|
|
422
|
+
await report(`Discarding transcript ${id}.`);
|
|
423
|
+
// Not through `request` above, because a 404 is a SUCCESS here — see below.
|
|
424
|
+
// `stepFetch` for the same reason it does; only the status handling differs.
|
|
425
|
+
const response = await stepFetch(`${TRANSCRIPT_ENDPOINT}/${id}`, {
|
|
426
|
+
method: "DELETE",
|
|
427
|
+
headers: { authorization: requireStepEnv(API_KEY_ENV) },
|
|
428
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
429
|
+
});
|
|
430
|
+
if (response.status === 404) return;
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
throw toStepError(
|
|
433
|
+
response,
|
|
434
|
+
`DELETE ${TRANSCRIPT_ENDPOINT}/${id} failed: HTTP ${response.status}`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Reduce a transcript to something worth saying out loud.
|
|
441
|
+
*
|
|
442
|
+
* Separate from the polling on purpose, and for the same reason `link-digest`
|
|
443
|
+
* splits its fetch from its summary: a rate-limited model call replays the whole
|
|
444
|
+
* transcription — twenty minutes of provider time, already paid for — out of the
|
|
445
|
+
* journal instead of submitting the recording again.
|
|
446
|
+
*/
|
|
447
|
+
export async function summarize(url: string, transcript: TranscriptState): Promise<Recap> {
|
|
448
|
+
"use step";
|
|
449
|
+
|
|
450
|
+
await report("Writing the recap.");
|
|
451
|
+
|
|
452
|
+
const text = (transcript.text ?? "").slice(0, MAX_TRANSCRIPT_CHARS);
|
|
453
|
+
if (text.trim() === "") {
|
|
454
|
+
// Not transient: the same completed transcript holds the same nothing on
|
|
455
|
+
// every attempt. Silence, or an audio file with no speech in it.
|
|
456
|
+
throw new FatalError("That recording came back with no speech in it.");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// `stepGenerateJson` unwraps the fence a model puts around JSON however firmly
|
|
460
|
+
// it is told not to, parses it, and validates it — all four things this step
|
|
461
|
+
// used to re-derive. `throwStepError` is what makes a terminal gateway failure
|
|
462
|
+
// (a bad key, a rejected request) stop rather than burn the remaining
|
|
463
|
+
// attempts, where a reply that missed the SHAPE throws plainly and retries.
|
|
464
|
+
const parsed = await stepGenerateJson(text, {
|
|
465
|
+
schema: RecapReply,
|
|
466
|
+
system:
|
|
467
|
+
"You write up recordings for someone who will hear the result on a phone call. " +
|
|
468
|
+
`Reply with JSON only: {"headline": string, "points": string[], "spoken": string}. ` +
|
|
469
|
+
`Give exactly ${POINTS} points. "spoken" is ONE sentence, under 30 words, ` +
|
|
470
|
+
"written to be read aloud. No markdown fence, no preamble.",
|
|
471
|
+
}).catch(throwStepError);
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
url,
|
|
475
|
+
headline: parsed.headline,
|
|
476
|
+
points: parsed.points.slice(0, POINTS),
|
|
477
|
+
spoken: parsed.spoken,
|
|
478
|
+
minutes: Math.round((transcript.audioDuration ?? 0) / 60),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** A rate limit — and a model that ignored the format — are both expected here. */
|
|
483
|
+
summarize.maxRetries = 5;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Say one line into the run's progress channel.
|
|
487
|
+
*
|
|
488
|
+
* A step for one reason: the body REPLAYS, so a `report()` written there is
|
|
489
|
+
* re-emitted on every resume. Everything the body itself wants to narrate —
|
|
490
|
+
* the slow-recording note, the unwind — comes through here, and `agent.ts`'s
|
|
491
|
+
* `recap_progress` is what reads it back down the phone.
|
|
492
|
+
*/
|
|
493
|
+
export async function note(line: string): Promise<void> {
|
|
494
|
+
"use step";
|
|
495
|
+
await report(line);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---- HTTP and parsing -------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* One authenticated request to the pre-recorded API, with this desk's retry
|
|
502
|
+
* policy on it.
|
|
503
|
+
*
|
|
504
|
+
* Note the header is a bare key: AssemblyAI's `authorization` takes the key
|
|
505
|
+
* itself, with no `Bearer` prefix.
|
|
506
|
+
*/
|
|
507
|
+
async function request(
|
|
508
|
+
url: string,
|
|
509
|
+
init: { method?: string; body?: string } = {},
|
|
510
|
+
): Promise<Response> {
|
|
511
|
+
// `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent runs (and
|
|
512
|
+
// this workflow POLLS, so one run is many requests) get a socket each rather
|
|
513
|
+
// than N streams on one connection — and a connection failure arrives as a
|
|
514
|
+
// `StepTransportError` naming its cause instead of a bare
|
|
515
|
+
// `TypeError: fetch failed`, which for a template whose whole subject is
|
|
516
|
+
// durability is the difference between a diagnosable resume and a mystery.
|
|
517
|
+
// `sdk/step-fetch.ts` carries the measurements.
|
|
518
|
+
const response = await stepFetch(url, {
|
|
519
|
+
...init,
|
|
520
|
+
headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
|
|
521
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
522
|
+
});
|
|
523
|
+
// The three-way retry decision, made by the SDK: a 401 or a 400 answers the
|
|
524
|
+
// same way on the fourth attempt and burns the step, a 429 or a 5xx is what
|
|
525
|
+
// retries are for, and a `Retry-After` the provider named is waited out rather
|
|
526
|
+
// than replaced by the DevKit's one-second default — which matters here more
|
|
527
|
+
// than usual, because a fan-out of segments hits a rate limit together.
|
|
528
|
+
if (!response.ok) {
|
|
529
|
+
throw toStepError(response, `${init.method ?? "GET"} ${url} failed: HTTP ${response.status}`);
|
|
530
|
+
}
|
|
531
|
+
return response;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** A string field of a JSON body, when it really is one. */
|
|
535
|
+
function readString(body: unknown, key: string): string | undefined {
|
|
536
|
+
const value = isRecord(body) ? body[key] : undefined;
|
|
537
|
+
return typeof value === "string" ? value : undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** A number field of a JSON body, when it really is one. */
|
|
541
|
+
function readNumber(body: unknown, key: string): number | undefined {
|
|
542
|
+
const value = isRecord(body) ? body[key] : undefined;
|
|
543
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
544
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* The hook tokens this desk's body and its tools have to agree on.
|
|
4
|
+
*
|
|
5
|
+
* A hook's token is chosen by the WORKFLOW and typed in by whatever signals it,
|
|
6
|
+
* so it is the one string two files must derive identically — and a template
|
|
7
|
+
* literal written twice is a string that drifts once, silently: the body waits
|
|
8
|
+
* on a token nobody signals, the tool signals a token nobody holds, and the
|
|
9
|
+
* only symptom is `ctx.workflows.signal` answering `false`, which is also what
|
|
10
|
+
* the ordinary "nobody is waiting" case looks like.
|
|
11
|
+
*
|
|
12
|
+
* Hence one exported function per token, imported by both sides. It carries no
|
|
13
|
+
* directive, which is what lets it live under `workflows/` beside the bodies:
|
|
14
|
+
* the WDK builder scans this directory and transforms only what carries one
|
|
15
|
+
* (`transcription-workflow/workflows/wav.ts` is the same shape).
|
|
16
|
+
*
|
|
17
|
+
* **A token addresses a run, so it is derived from the SESSION** rather than
|
|
18
|
+
* from anything a caller could name out loud. `ctx.sessionId` keys this call;
|
|
19
|
+
* a real desk keys on the caller's number, and the run's `requestedBy` moves
|
|
20
|
+
* with it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The gate that asks whether the transcript stays on file. */
|
|
24
|
+
export function retentionToken(sessionId: string): string {
|
|
25
|
+
return `retention:${sessionId}`;
|
|
26
|
+
}
|