@alexkroman1/aai-cli 13.3.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  4. package/dist/_artifacts.d.ts +16 -0
  5. package/dist/_build-target.d.ts +172 -0
  6. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
  7. package/dist/_cli-common.d.ts +45 -6
  8. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  9. package/dist/_dev-env.d.ts +1 -1
  10. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
  11. package/dist/_dev-vite-config.d.ts +3 -3
  12. package/dist/_e2e-test-utils.d.ts +1 -1
  13. package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
  14. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  15. package/dist/_output.d.ts +10 -9
  16. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  17. package/dist/_resource-commands.d.ts +9 -9
  18. package/dist/_server-common-De0haHr9.mjs +70 -0
  19. package/dist/_server-common.d.ts +20 -1
  20. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  21. package/dist/_slug-api.d.ts +19 -0
  22. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  23. package/dist/_studio-commands.d.ts +13 -13
  24. package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
  25. package/dist/_templates.d.ts +1 -1
  26. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  27. package/dist/_typecheck-gate.d.ts +3 -1
  28. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  29. package/dist/_utils.d.ts +35 -0
  30. package/dist/_vercel-output.d.ts +63 -0
  31. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  32. package/dist/_workflow-determinism.d.ts +3 -3
  33. package/dist/build-BhEaxBPu.mjs +481 -0
  34. package/dist/build.d.ts +23 -10
  35. package/dist/cli.d.ts +1 -2
  36. package/dist/cli.mjs +155 -138
  37. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
  38. package/dist/client-bundler.d.ts +1 -1
  39. package/dist/client-bundler.mjs +1 -1
  40. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  41. package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
  42. package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
  43. package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
  44. package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
  45. package/dist/init.d.ts +14 -0
  46. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  47. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  48. package/dist/project-config.mjs +1 -1
  49. package/dist/scaffold/CLAUDE.md +155 -76
  50. package/dist/scaffold/package.json +6 -6
  51. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  52. package/dist/secret.d.ts +8 -6
  53. package/dist/start.d.ts +112 -0
  54. package/dist/start.mjs +156 -0
  55. package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
  56. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  57. package/dist/templates/call-audit/agent.test.ts +11 -6
  58. package/dist/templates/call-audit/agent.ts +2 -2
  59. package/dist/templates/call-audit/client.tsx +27 -42
  60. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  61. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  62. package/dist/templates/call-audit/workflows/media.ts +12 -37
  63. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  64. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  65. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  66. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  67. package/dist/templates/dispatch-center/client.tsx +53 -61
  68. package/dist/templates/dispatch-center/shared.ts +0 -8
  69. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  70. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  71. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  72. package/dist/templates/infocom-adventure/client.tsx +40 -36
  73. package/dist/templates/link-digest/agent.test.ts +3 -3
  74. package/dist/templates/link-digest/agent.ts +2 -2
  75. package/dist/templates/link-digest/client.tsx +81 -19
  76. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  77. package/dist/templates/math-buddy/agent.test.ts +126 -0
  78. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  79. package/dist/templates/night-owl/client.tsx +8 -2
  80. package/dist/templates/personal-finance/agent.test.ts +127 -0
  81. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  82. package/dist/templates/pizza-ordering/client.tsx +5 -4
  83. package/dist/templates/pizza-ordering/shared.ts +3 -8
  84. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  85. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  86. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  87. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  88. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  89. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  90. package/dist/templates/plan-and-execute/client.tsx +2 -2
  91. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  92. package/dist/templates/podcast-digest/client.tsx +16 -22
  93. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  94. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  95. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  96. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  97. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  98. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  99. package/dist/templates/redline/agent.eval.test.ts +5 -8
  100. package/dist/templates/redline/agent.test.ts +1 -1
  101. package/dist/templates/redline/client.tsx +36 -34
  102. package/dist/templates/redline/workflows/redline.ts +17 -17
  103. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  104. package/dist/templates/research-workflow/agent.test.ts +10 -7
  105. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  106. package/dist/templates/retail/agent.test.ts +17 -15
  107. package/dist/templates/retail/cancel.ts +7 -6
  108. package/dist/templates/retail/client.tsx +39 -58
  109. package/dist/templates/retail/payment.ts +4 -3
  110. package/dist/templates/retail/pending.test.ts +19 -19
  111. package/dist/templates/retail/store.test.ts +5 -5
  112. package/dist/templates/retail/swap.ts +8 -11
  113. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  114. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  115. package/dist/templates/solo-rpg/client.tsx +3 -3
  116. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  117. package/dist/templates/spoken-summary/agent.ts +3 -3
  118. package/dist/templates/spoken-summary/client.tsx +23 -30
  119. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  120. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  121. package/dist/templates/support-line/agent.ts +8 -0
  122. package/dist/templates/support-line/client.tsx +4 -3
  123. package/dist/templates/support-line/prompts.ts +0 -2
  124. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  125. package/dist/templates/transcription-workflow/agent.ts +2 -2
  126. package/dist/templates/transcription-workflow/client.tsx +15 -14
  127. package/dist/templates/transcription-workflow/recover.ts +1 -1
  128. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  129. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  130. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  131. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  132. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  133. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  134. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  135. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  136. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  137. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  138. package/dist/templates/travel-concierge/client.tsx +2 -2
  139. package/dist/templates/travel-concierge/routing.ts +64 -55
  140. package/dist/templates/travel-concierge/shared.ts +10 -14
  141. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  142. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  143. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  144. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  145. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  146. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  147. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  148. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  149. package/dist/templates/web-researcher/agent.test.ts +130 -0
  150. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  151. package/dist/typecheck.mjs +1 -1
  152. package/dist/worker-bundler.d.ts +1 -1
  153. package/dist/worker-bundler.mjs +8 -8
  154. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  155. package/package.json +9 -4
  156. package/dist/_server-common-BV4_CrP3.mjs +0 -43
  157. package/dist/build-CWyFGe-W.mjs +0 -69
  158. package/dist/scaffold/server.mjs +0 -204
@@ -7,7 +7,7 @@
7
7
  * webhook shapes and the branch between them, Block Kit assembly, mrkdwn
8
8
  * escaping, the 4xx/5xx split and the advice each refusal deserves. All of it
9
9
  * is `@alexkroman1/aai/channels` now — `slackChannel()` names the destination,
10
- * `sendToChannelClassified` posts and classifies — and what is left here is the
10
+ * `sendToChannelOrFail` posts and classifies — and what is left here is the
11
11
  * part that is actually about podcasts: turning episode digests into a
12
12
  * {@link ChannelMessage}.
13
13
  *
@@ -22,13 +22,13 @@
22
22
  * The step BOUNDARY is not here — only a body holds a `ctx`, and the call this
23
23
  * file is reached through is `ctx.step("postDigest", …)` in `digest.ts`. What
24
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
25
+ * one `sendToChannelOrFail` call. Deciding which steps exist is the
26
26
  * template's job; what happens inside one is the SDK's.
27
27
  */
28
28
 
29
29
  import { type ChannelMessage, slackChannel } from "@alexkroman1/aai/channels";
30
- import { report } from "@alexkroman1/aai/step";
31
- import { sendToChannelClassified } from "@alexkroman1/aai/step-errors";
30
+ import { stepReport } from "@alexkroman1/aai/step";
31
+ import { sendToChannelOrFail } from "@alexkroman1/aai/step-errors";
32
32
  import type { EpisodeDigest } from "./digest.ts";
33
33
 
34
34
  /** Everything the message needs, so rendering can stay a pure function. */
@@ -46,14 +46,14 @@ export type SlackDigestInput = {
46
46
  *
47
47
  * It is three lines because the interesting decisions moved. `slackChannel()` builds
48
48
  * the descriptor, {@link renderDigestMessage} says what the message contains,
49
- * and `sendToChannelClassified` does the render-post-classify round — throwing
49
+ * and `sendToChannelOrFail` does the render-post-classify round — throwing
50
50
  * a `FatalError` on a 4xx (a revoked webhook and a wrong variable name answer
51
51
  * identically on every retry, so retrying only delays the real error) and a
52
52
  * `RetryableError` carrying Slack's own `Retry-After` on a 5xx.
53
53
  */
54
54
  export async function sendDigestToSlack(input: SlackDigestInput): Promise<string> {
55
- await report("Posting the digest to Slack.");
56
- return await sendToChannelClassified(
55
+ await stepReport("Posting the digest to Slack.");
56
+ return await sendToChannelOrFail(
57
57
  slackChannel({ webhookUrl: input.slackWebhookUrl, textParam: input.slackWorkflowTextParam }),
58
58
  renderDigestMessage(input),
59
59
  );
@@ -40,7 +40,7 @@
40
40
  //
41
41
  // What no eval here can show is a delivery RESUMING a run, so no case claims
42
42
  // it. The answered arm is `agent.test.ts`'s, through
43
- // `createWorkflowCtx({ hooks })`, which is the only tier that can send a
43
+ // `createWorkflowContext({ hooks })`, which is the only tier that can send a
44
44
  // payload at all; a real HTTP POST to the public callback route is
45
45
  // `aai-cli`'s `dev-workflow.scenario.test.ts`'s, and is not yet written.
46
46
  // * **The provider is scripted, in BOTH modes**, through `stepFetch`'s
@@ -62,7 +62,7 @@
62
62
  * this file SHIPS — see `agent.test.ts`.
63
63
  */
64
64
  import agentDef from "virtual:aai/agent";
65
- import { stubGatewayRoute } from "@alexkroman1/aai/testing";
65
+ import { routeStepFetch, type StepRoute, stubGatewayRoute } from "@alexkroman1/aai/testing";
66
66
  import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
67
67
  import {
68
68
  describeToolCalls,
@@ -138,22 +138,30 @@ function stubProvider(options: { hold?: boolean; ending?: Ending } = {}): Script
138
138
  const gate = Promise.withResolvers<void>();
139
139
  const model = stubGatewayRoute(RECAP_JSON);
140
140
  let polls = 0;
141
- const stub = installStubStepFetch(async (request) => {
142
- const recapped = model.route(request);
143
- if (recapped) return recapped;
141
+ // Two legs: the model, then the provider's job API. Anything neither answers is a
142
+ // finding, which is `routeStepFetch`'s default rather than this file's throw.
143
+ // Annotated, so the leg's contract is visible where it is written: answer the
144
+ // requests you recognise, `undefined` for everything else, and let
145
+ // `routeStepFetch` decide what an unrecognised one means.
146
+ const provider: StepRoute = (request) => {
144
147
  if (request.method === "POST") return { body: { id: TRANSCRIPT_ID, status: "queued" } };
145
148
  // The compensation. A real DELETE removes the transcript from the account,
146
149
  // which is what makes "a failed run leaves nothing behind" a claim rather
147
150
  // than a comment — so the assertion that matters is that this was CALLED.
148
151
  if (request.method === "DELETE") return { body: {} };
149
- if (request.method === "GET") {
150
- polls += 1;
151
- if (options.hold === true && polls === 1) await gate.promise;
152
- return options.ending === "error"
153
- ? { body: { status: "error", error: "that recording could not be decoded" } }
154
- : { body: { status: "completed", text: TRANSCRIPT_TEXT, audio_duration: 254 } };
155
- }
156
- throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
152
+ if (request.method !== "GET") return;
153
+ polls += 1;
154
+ return options.ending === "error"
155
+ ? { body: { status: "error", error: "that recording could not be decoded" } }
156
+ : { body: { status: "completed", text: TRANSCRIPT_TEXT, audio_duration: 254 } };
157
+ };
158
+ const route = routeStepFetch([model.route, provider]);
159
+ const stub = installStubStepFetch(async (request) => {
160
+ const answered = route(request);
161
+ // Held AFTER routing, so the poll this returns is the one the script owed
162
+ // it — and `polls` has already counted this request.
163
+ if (options.hold === true && request.method === "GET" && polls === 1) await gate.promise;
164
+ return answered;
157
165
  });
158
166
  return { calls: stub.calls, release: () => gate.resolve() };
159
167
  }
@@ -33,7 +33,7 @@
33
33
  * **The branch this file used to name as its biggest gap no longer exists.** It
34
34
  * was `recapFlow`'s `if (isWorkflowSuspend(err)) throw err;` — the guard whose
35
35
  * absence had once deleted the transcript the run was waiting for — and it was
36
- * unpinnable here by construction, since `createWorkflowCtx`'s `sleep` is
36
+ * unpinnable here by construction, since `createWorkflowContext`'s `sleep` is
37
37
  * RECORDED and its `waitFor` answers out of `hooks`, so no wait it serves ever
38
38
  * suspended. A wait now hands the body a promise that never settles, so a
39
39
  * suspension cannot reach a `catch` at all and there is no branch left to test:
@@ -47,7 +47,7 @@ import type { WorkflowClient } from "@alexkroman1/aai";
47
47
  import {
48
48
  createRunSnapshot,
49
49
  createToolContext,
50
- createWorkflowCtx,
50
+ createWorkflowContext,
51
51
  parseSchemaInput,
52
52
  schemaInputIssues,
53
53
  stubGatewayRoute,
@@ -55,7 +55,7 @@ import {
55
55
  } from "@alexkroman1/aai/testing";
56
56
  import {
57
57
  installStubStepFetch,
58
- mockWorkflows,
58
+ installStubWorkflows,
59
59
  installStubGateway as stubGateway,
60
60
  } from "@alexkroman1/aai/testing/vitest";
61
61
  import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
@@ -89,14 +89,14 @@ const run = toolRunner(agentDef);
89
89
  /**
90
90
  * A `ctx.workflows` that records `start` and answers the lookups from a fixture.
91
91
  *
92
- * `mockWorkflows` (`@alexkroman1/aai/testing/vitest`) is the whole thing — a
92
+ * `installStubWorkflows` (`@alexkroman1/aai/testing/vitest`) is the whole thing — a
93
93
  * `vi.fn` per method over one `runs` list, with `stream`/`streamTail` left
94
94
  * rejecting because `recap_progress` reads progress through `lastLine` and
95
95
  * composing those two by hand is the hazard `lastLine` exists to remove. What
96
96
  * is local is only which workflow this desk declares.
97
97
  */
98
98
  function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
99
- return mockWorkflows({ runs, names: ["recap"] });
99
+ return installStubWorkflows({ runs, names: ["recap"] });
100
100
  }
101
101
 
102
102
  /** A finished recap, as the workflow's output reaches the tools. */
@@ -399,7 +399,7 @@ describe("cancel_recap", () => {
399
399
  *
400
400
  * Published into `stepFetch`'s OWN slot, not over `globalThis.fetch`. Every
401
401
  * request in this file goes through `stepFetch` — `request()` and
402
- * `discardTranscript` reach it directly, `stepTranscribeSubmitClassified`
402
+ * `discardTranscript` reach it directly, `stepTranscribeSubmitOrFail`
403
403
  * through the SDK — and `step-fetch.ts` falls back to `globalThis.fetch` only
404
404
  * when nothing is published. A global stub therefore passed while exercising a
405
405
  * path production never takes; every sibling template already stubs the slot,
@@ -686,7 +686,7 @@ describe("summarize", () => {
686
686
  // The policy is an argument to `ctx.step` now, so it is observable only at
687
687
  // the call. `runSteps: false` and a skeleton of results: the subject is what
688
688
  // the body ASKED FOR.
689
- const ctx = createWorkflowCtx({
689
+ const ctx = createWorkflowContext({
690
690
  runSteps: false,
691
691
  results: {
692
692
  submitRecording: { id: "t_1" },
@@ -724,7 +724,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
724
724
  { status: "processing" },
725
725
  { status: "completed", text: "Done.", audio_duration: 60 },
726
726
  ]);
727
- const state = await awaitTranscript("t_1", createWorkflowCtx());
727
+ const state = await awaitTranscript("t_1", createWorkflowContext());
728
728
  expect(state).toMatchObject({ status: "completed", text: "Done." });
729
729
  expect(polls()).toBe(3);
730
730
  });
@@ -733,13 +733,15 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
733
733
  // `error` is terminal: polling a failed job to the bound would spend twenty
734
734
  // minutes learning what the first answer already said.
735
735
  const polls = stubStatuses([{ status: "error", error: "Transcoding failed" }]);
736
- await expect(awaitTranscript("t_1", createWorkflowCtx())).rejects.toThrow(/Transcoding failed/);
736
+ await expect(awaitTranscript("t_1", createWorkflowContext())).rejects.toThrow(
737
+ /Transcoding failed/,
738
+ );
737
739
  expect(polls()).toBe(1);
738
740
  });
739
741
 
740
742
  test("gives up at the bound rather than polling a stuck job forever", async () => {
741
743
  stubStatuses([{ status: "processing" }]);
742
- await expect(awaitTranscript("t_1", createWorkflowCtx())).rejects.toThrow(/Gave up/);
744
+ await expect(awaitTranscript("t_1", createWorkflowContext())).rejects.toThrow(/Gave up/);
743
745
  });
744
746
 
745
747
  /** A job still `processing` for `polls` turns, then completed. */
@@ -758,7 +760,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
758
760
  // the constant promises is eight sleeps, so the earliest honest turn to say
759
761
  // it is the ninth. Here the job finishes on that ninth poll, two minutes in
760
762
  // and not a second over, so the caller is told nothing.
761
- const ctx = createWorkflowCtx();
763
+ const ctx = createWorkflowContext();
762
764
  stubSlowJob(8);
763
765
 
764
766
  await awaitTranscript("t_1", ctx);
@@ -773,7 +775,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
773
775
  // two minutes of waiting behind it — so the note goes out, and the eleven
774
776
  // polls after it say nothing more. A note per poll would be a caller told
775
777
  // the same sentence every fifteen seconds.
776
- const ctx = createWorkflowCtx();
778
+ const ctx = createWorkflowContext();
777
779
  stubSlowJob(20);
778
780
 
779
781
  await awaitTranscript("t_1", ctx);
@@ -788,7 +790,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
788
790
  // cases are the REGRESSION guard for this section: the poll-only arm has to
789
791
  // keep behaving exactly as it did before there was a callback at all.
790
792
  //
791
- // `createWorkflowCtx` is the only tier that can drive the answered branch:
793
+ // `createWorkflowContext` is the only tier that can drive the answered branch:
792
794
  // its `waitFor` reads `hooks` by token, so supplying a payload IS the delivery
793
795
  // landing and omitting one IS the window closing. The eval tier cannot —
794
796
  // nothing there can signal — so it only ever sees the fallback, which is
@@ -806,7 +808,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
806
808
  { status: "processing" },
807
809
  { status: "completed", text: "Done.", audio_duration: 60 },
808
810
  ]);
809
- const ctx = createWorkflowCtx({
811
+ const ctx = createWorkflowContext({
810
812
  hooks: { [NUDGE]: { transcript_id: "t_1", status: "completed" } },
811
813
  });
812
814
 
@@ -831,7 +833,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
831
833
  { status: "processing" },
832
834
  { status: "completed", text: "The real transcript.", audio_duration: 60 },
833
835
  ]);
834
- const ctx = createWorkflowCtx({
836
+ const ctx = createWorkflowContext({
835
837
  hooks: { [NUDGE]: { transcript_id: "t_1", status: "completed", text: "A LIE." } },
836
838
  });
837
839
 
@@ -850,7 +852,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
850
852
  // read as a failed run and answer by deleting the transcript. So the wait
851
853
  // may not be inside the loop, and this is what pins that: twenty turns, ONE
852
854
  // `waitFor`, and every other wait a plain sleep.
853
- const ctx = createWorkflowCtx({ hooks: { [NUDGE]: {} } });
855
+ const ctx = createWorkflowContext({ hooks: { [NUDGE]: {} } });
854
856
  stubSlowJob(20);
855
857
 
856
858
  await awaitTranscript("t_1", ctx, NUDGE);
@@ -869,7 +871,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
869
871
  { status: "processing" },
870
872
  { status: "completed", text: "Done.", audio_duration: 60 },
871
873
  ]);
872
- const ctx = createWorkflowCtx();
874
+ const ctx = createWorkflowContext();
873
875
 
874
876
  const state = await awaitTranscript("t_1", ctx, NUDGE);
875
877
 
@@ -885,7 +887,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
885
887
  // window — so whichever arm a run is on, a caller hears the sentence at the
886
888
  // same point. The note goes out at the TOP of a poll, so attempt 2 is the
887
889
  // first turn with a whole closed window behind it.
888
- const ctx = createWorkflowCtx();
890
+ const ctx = createWorkflowContext();
889
891
  stubSlowJob(20);
890
892
 
891
893
  await awaitTranscript("t_1", ctx, NUDGE);
@@ -904,7 +906,7 @@ describe("awaitTranscript — the polling port, and the callback over it", () =>
904
906
  // The mirror of the case above, and what stops the note being a thing every
905
907
  // callback run says: the delivery lands, attempt 2 finds the job done, and
906
908
  // nobody is told a recording is a long one.
907
- const ctx = createWorkflowCtx({ hooks: { [NUDGE]: {} } });
909
+ const ctx = createWorkflowContext({ hooks: { [NUDGE]: {} } });
908
910
  stubSlowJob(1);
909
911
 
910
912
  await awaitTranscript("t_1", ctx, NUDGE);
@@ -930,7 +932,9 @@ describe("askWhetherToKeep — the expense port", () => {
930
932
  * What is pinned is unchanged: the three outcomes and the safe default.
931
933
  */
932
934
  const gateCtx = (answer?: { keep: boolean }) =>
933
- createWorkflowCtx(answer === undefined ? {} : { hooks: { [retentionToken("s_1")]: answer } });
935
+ createWorkflowContext(
936
+ answer === undefined ? {} : { hooks: { [retentionToken("s_1")]: answer } },
937
+ );
934
938
 
935
939
  test("keeps the transcript when the caller says to, and deletes nothing", async () => {
936
940
  const provider = installStubStepFetch();
@@ -1002,7 +1006,7 @@ describe("compensate — the saga port", () => {
1002
1006
  { label: "first", undo: async () => void order.push("first") },
1003
1007
  ],
1004
1008
  "because",
1005
- createWorkflowCtx(),
1009
+ createWorkflowContext(),
1006
1010
  );
1007
1011
  expect(order).toEqual(["second", "first"]);
1008
1012
  });
@@ -1024,7 +1028,7 @@ describe("compensate — the saga port", () => {
1024
1028
  { label: "fine", undo: async () => void order.push("fine") },
1025
1029
  ],
1026
1030
  "because",
1027
- createWorkflowCtx(),
1031
+ createWorkflowContext(),
1028
1032
  ),
1029
1033
  ).resolves.toBeUndefined();
1030
1034
  expect(order).toEqual(["fine"]);
@@ -1035,7 +1039,7 @@ describe("compensate — the saga port", () => {
1035
1039
  // run that narrated an unwind it did not perform would be lying to the
1036
1040
  // caller reading its progress.
1037
1041
  await expect(
1038
- compensate([], "nothing was acquired", createWorkflowCtx()),
1042
+ compensate([], "nothing was acquired", createWorkflowContext()),
1039
1043
  ).resolves.toBeUndefined();
1040
1044
  });
1041
1045
  });
@@ -99,7 +99,7 @@
99
99
  * Everything the desk claims to do. `submitRecording`, `checkTranscript` and
100
100
  * `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
101
101
  * `DELETE` on `/v2/transcript`), and `summarize` is a real model call through
102
- * `stepGenerateJsonClassified`. The BATCH API is what makes the polling port honest: it
102
+ * `stepGenerateJsonOrFail`. The BATCH API is what makes the polling port honest: it
103
103
  * answers with a job id in milliseconds and finishes minutes later, so the wait
104
104
  * is the provider's, not a `setTimeout` this template chose. (Its sibling
105
105
  * `transcription-workflow` takes the other endpoint — the sync one, which answers in
@@ -111,13 +111,13 @@
111
111
  * just your shell.
112
112
  */
113
113
 
114
- import type { WorkflowCtx } from "@alexkroman1/aai";
115
- import { report, requireStepEnv, stepFetch, stepWebhookUrl } from "@alexkroman1/aai/step";
114
+ import type { WorkflowContext } from "@alexkroman1/aai";
115
+ import { requireStepEnv, stepFetch, stepReport, stepWebhookUrl } from "@alexkroman1/aai/step";
116
116
  import {
117
117
  FatalError,
118
- stepFetchOk,
119
- stepGenerateJsonClassified,
120
- stepTranscribeSubmitClassified,
118
+ stepFetchOrFail,
119
+ stepGenerateJsonOrFail,
120
+ stepTranscribeSubmitOrFail,
121
121
  toStepError,
122
122
  } from "@alexkroman1/aai/step-errors";
123
123
  import { errorMessage, omitUndefined } from "@alexkroman1/aai/utils";
@@ -239,7 +239,7 @@ const POINTS = 3;
239
239
  /**
240
240
  * The shape the model must answer in.
241
241
  *
242
- * `stepGenerateJsonClassified` validates against this and throws PLAINLY when the reply
242
+ * `stepGenerateJsonOrFail` validates against this and throws PLAINLY when the reply
243
243
  * misses, which is the retry policy in one distinction: a model that answered in
244
244
  * prose may answer correctly next time, where a 401 will not. `spoken` is the
245
245
  * field this template exists for — without it the announced turn has nothing to
@@ -347,7 +347,7 @@ export type Compensation = { label: string; undo: () => Promise<void> };
347
347
  * `agent.ts` reads back down the phone, so it is shaped for an ear rather than
348
348
  * a page.
349
349
  */
350
- export async function recapFlow(input: { url: string; requestedBy: string }, ctx: WorkflowCtx) {
350
+ export async function recapFlow(input: { url: string; requestedBy: string }, ctx: WorkflowContext) {
351
351
  // The compensation stack, newest first — `unshift` after each successful
352
352
  // acquisition, exactly as Temporal's `openAccount` does. Registering the undo
353
353
  // AFTER the step it undoes is the whole discipline: a step that never
@@ -462,7 +462,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }, ctx
462
462
  */
463
463
  export async function awaitTranscript(
464
464
  id: string,
465
- ctx: WorkflowCtx,
465
+ ctx: WorkflowContext,
466
466
  nudge?: string,
467
467
  ): Promise<TranscriptState> {
468
468
  // Which turn says "still going". A pure function of `nudge`, which the body
@@ -518,7 +518,7 @@ export async function askWhetherToKeep(
518
518
  requestedBy: string,
519
519
  transcriptId: string,
520
520
  compensations: Compensation[],
521
- ctx: WorkflowCtx,
521
+ ctx: WorkflowContext,
522
522
  ): Promise<Retention> {
523
523
  await ctx.step("noteGate", () =>
524
524
  note(
@@ -560,7 +560,7 @@ export async function askWhetherToKeep(
560
560
  export async function compensate(
561
561
  compensations: Compensation[],
562
562
  because: string,
563
- ctx: WorkflowCtx,
563
+ ctx: WorkflowContext,
564
564
  ): Promise<void> {
565
565
  if (compensations.length === 0) return;
566
566
  // The narration is a STEP like every other, so an unwind interrupted by a
@@ -607,9 +607,9 @@ export async function submitRecording(
607
607
  url: string,
608
608
  webhookUrl?: string,
609
609
  ): Promise<{ id: string; callback: boolean }> {
610
- await report(`Submitting ${new URL(url).hostname} for transcription…`);
610
+ await stepReport(`Submitting ${new URL(url).hostname} for transcription…`);
611
611
 
612
- // `stepTranscribeSubmitClassified` owns the endpoint, the raw-key auth, the
612
+ // `stepTranscribeSubmitOrFail` owns the endpoint, the raw-key auth, the
613
613
  // PLURAL `speech_models` field and the failure classification — the
614
614
  // `Classified` suffix being that last part: it is `stepTranscribeSubmit` with
615
615
  // `throwStepError` already applied, so a provider refusal stays terminal and a
@@ -625,7 +625,7 @@ export async function submitRecording(
625
625
  // What must not creep in is a `?? null` or a `?? ""` to "be explicit": either
626
626
  // one puts the key back, and a provider handed a null for a URL is entitled to
627
627
  // refuse the whole submission.
628
- const job = await stepTranscribeSubmitClassified(url, {
628
+ const job = await stepTranscribeSubmitOrFail(url, {
629
629
  params: { speaker_labels: true, webhook_url: webhookUrl },
630
630
  });
631
631
  return { id: job.id, callback: webhookUrl !== undefined };
@@ -698,7 +698,7 @@ export async function checkTranscript(id: string): Promise<TranscriptState> {
698
698
  );
699
699
  }
700
700
 
701
- await report(`Transcript ${parsed.data.status}.`);
701
+ await stepReport(`Transcript ${parsed.data.status}.`);
702
702
  return {
703
703
  status: parsed.data.status,
704
704
  // `omitUndefined` rather than a spread-ternary per field: under
@@ -730,7 +730,7 @@ export async function checkTranscript(id: string): Promise<TranscriptState> {
730
730
  * a replay is exactly that world.
731
731
  */
732
732
  export async function discardTranscript(id: string): Promise<void> {
733
- await report(`Discarding transcript ${id}.`);
733
+ await stepReport(`Discarding transcript ${id}.`);
734
734
  // Not through `request` above, because a 404 is a SUCCESS here — see below.
735
735
  // `stepFetch` for the same reason it does; only the status handling differs.
736
736
  const response = await stepFetch(`${TRANSCRIPT_ENDPOINT}/${id}`, {
@@ -756,7 +756,7 @@ export async function discardTranscript(id: string): Promise<void> {
756
756
  * journal instead of submitting the recording again.
757
757
  */
758
758
  export async function summarize(url: string, transcript: TranscriptState): Promise<Recap> {
759
- await report("Writing the recap.");
759
+ await stepReport("Writing the recap.");
760
760
 
761
761
  const text = (transcript.text ?? "").slice(0, MAX_TRANSCRIPT_CHARS);
762
762
  if (text.trim() === "") {
@@ -765,13 +765,13 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
765
765
  throw new FatalError("That recording came back with no speech in it.");
766
766
  }
767
767
 
768
- // `stepGenerateJsonClassified` unwraps the fence a model puts around JSON
768
+ // `stepGenerateJsonOrFail` unwraps the fence a model puts around JSON
769
769
  // however firmly it is told not to, parses it, and validates it — all four
770
770
  // things this step used to re-derive. The `Classified` half is what makes a
771
771
  // terminal gateway failure (a bad key, a rejected request) stop rather than
772
772
  // burn the remaining attempts, where a reply that missed the SHAPE throws
773
773
  // plainly and retries.
774
- const parsed = await stepGenerateJsonClassified(text, {
774
+ const parsed = await stepGenerateJsonOrFail(text, {
775
775
  schema: RecapReply,
776
776
  system:
777
777
  "You write up recordings for someone who will hear the result on a phone call. " +
@@ -792,13 +792,13 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
792
792
  /**
793
793
  * Say one line into the run's progress channel.
794
794
  *
795
- * A step for one reason: the body REPLAYS, so a `report()` written there is
795
+ * A step for one reason: the body REPLAYS, so a `stepReport()` written there is
796
796
  * re-emitted on every resume. Everything the body itself wants to narrate —
797
797
  * the slow-recording note, the unwind — comes through here, and `agent.ts`'s
798
798
  * `recap_progress` is what reads it back down the phone.
799
799
  */
800
800
  export async function note(line: string): Promise<void> {
801
- await report(line);
801
+ await stepReport(line);
802
802
  }
803
803
 
804
804
  // ---- HTTP -------------------------------------------------------------------
@@ -810,10 +810,7 @@ export async function note(line: string): Promise<void> {
810
810
  * Note the header is a bare key: AssemblyAI's `authorization` takes the key
811
811
  * itself, with no `Bearer` prefix.
812
812
  */
813
- async function request(
814
- url: string,
815
- init: { method?: string; body?: string } = {},
816
- ): Promise<Response> {
813
+ async function request(url: string): Promise<Response> {
817
814
  // Through `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
818
815
  // runs (and this workflow POLLS, so one run is many requests) get a socket
819
816
  // each rather than N streams on one connection — and a connection failure
@@ -821,14 +818,13 @@ async function request(
821
818
  // `TypeError: fetch failed`, which for a template whose whole subject is
822
819
  // durability is the difference between a diagnosable resume and a mystery.
823
820
  // `sdk/step-fetch.ts` carries the measurements.
824
- // `stepFetchOk` makes the three-way retry decision: a 401 or a 400 answers the
821
+ // `stepFetchOrFail` makes the three-way retry decision: a 401 or a 400 answers the
825
822
  // same way on the fourth attempt and burns the step, a 429 or a 5xx is what
826
823
  // retries are for, and a `Retry-After` the provider named is waited out rather
827
824
  // than replaced by the DevKit's one-second default — which matters here more
828
825
  // than usual, because a fan-out of segments hits a rate limit together. The
829
826
  // DELETE below stays on plain `stepFetch`, because there a 404 is a SUCCESS.
830
- return await stepFetchOk(url, {
831
- ...init,
827
+ return await stepFetchOrFail(url, {
832
828
  headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
833
829
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
834
830
  });
@@ -29,7 +29,7 @@
29
29
  // journal, no replay, and no per-step retry, so a rate-limited live run FAILS
30
30
  // where a deployed one would have ridden it out. The tier that really resumes a
31
31
  // run is `aai-cli`'s `dev-workflow.scenario.test.ts`.
32
- import { stubGatewayRoute } from "@alexkroman1/aai/testing";
32
+ import { routeStepFetch, stubGatewayRoute } from "@alexkroman1/aai/testing";
33
33
  import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
34
34
  import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
35
35
  import { expect } from "vitest";
@@ -75,13 +75,10 @@ const critique = (verdict: "ship" | "revise", score = 8): string =>
75
75
  */
76
76
  function scriptGateway(contents: readonly string[]) {
77
77
  const model = stubGatewayRoute(contents);
78
- installStubStepFetch((request) => {
79
- const answered = model.route(request);
80
- if (answered === undefined) {
81
- throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
82
- }
83
- return answered;
84
- });
78
+ // `routeStepFetch` defaults to throwing on an unrecognised request, which is
79
+ // what this file wants and what it used to spell out: every step in this body
80
+ // is a model call, so anything the route does not recognise is a finding.
81
+ installStubStepFetch(routeStepFetch([model.route]));
85
82
  return model;
86
83
  }
87
84
 
@@ -228,7 +228,7 @@ describe("the steps", () => {
228
228
  * to take the same branch on every walk.
229
229
  *
230
230
  * The model is the whole world here (`writeDraft`, `critiqueDraft` and
231
- * `reviseDraft` are all `stepGenerate*Classified`), so `stubGateway`'s scripted
231
+ * `reviseDraft` are all `stepGenerate*OrFail`), so `stubGateway`'s scripted
232
232
  * replies ARE the run, and its call log is what proves a replay did not pay for
233
233
  * a round twice. Scripted in body order, with the last reply repeating.
234
234
  */