@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.
Files changed (234) hide show
  1. package/README.md +1 -0
  2. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  3. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
  4. package/dist/_dev-env.d.ts +22 -2
  5. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
  6. package/dist/_dev-server.d.ts +0 -17
  7. package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
  8. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  9. package/dist/_preflight.d.ts +1 -1
  10. package/dist/_resource-commands.d.ts +23 -0
  11. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
  12. package/dist/_slug-api.d.ts +5 -2
  13. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  14. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
  15. package/dist/_templates.d.ts +3 -4
  16. package/dist/_workflow-scan.d.ts +108 -0
  17. package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
  18. package/dist/cli.mjs +346 -349
  19. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  20. package/dist/client-bundler.mjs +1 -1
  21. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  22. package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
  23. package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
  24. package/dist/eval-DfQ8cm5p.mjs +56 -0
  25. package/dist/eval.d.ts +43 -0
  26. package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
  27. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  28. package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
  29. package/dist/project-config.mjs +1 -1
  30. package/dist/scaffold/CLAUDE.md +442 -80
  31. package/dist/scaffold/package.json +6 -3
  32. package/dist/scaffold/server.mjs +11 -4
  33. package/dist/scaffold/tsconfig.json +0 -1
  34. package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
  35. package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
  36. package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
  37. package/dist/templates/briefing-desk/agent.test.ts +350 -0
  38. package/dist/templates/briefing-desk/agent.ts +30 -0
  39. package/dist/templates/briefing-desk/shared.ts +200 -0
  40. package/dist/templates/briefing-desk/system-prompt.md +39 -0
  41. package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
  42. package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
  43. package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
  44. package/dist/templates/call-audit/agent.eval.test.ts +474 -0
  45. package/dist/templates/call-audit/agent.test.ts +57 -204
  46. package/dist/templates/call-audit/agent.ts +32 -19
  47. package/dist/templates/call-audit/client.tsx +17 -54
  48. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  49. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  50. package/dist/templates/call-audit/workflows/media.ts +2 -12
  51. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  52. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  53. package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
  54. package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
  55. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  56. package/dist/templates/dispatch-center/client.tsx +239 -129
  57. package/dist/templates/dispatch-center/shared.ts +99 -1
  58. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  59. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  60. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  61. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  62. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  63. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  66. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  67. package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
  68. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  69. package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
  70. package/dist/templates/health-assistant/agent.test.ts +22 -4
  71. package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
  72. package/dist/templates/infocom-adventure/agent.test.ts +53 -24
  73. package/dist/templates/infocom-adventure/agent.ts +21 -0
  74. package/dist/templates/infocom-adventure/client.tsx +246 -164
  75. package/dist/templates/infocom-adventure/shared.ts +20 -9
  76. package/dist/templates/infocom-adventure/system-prompt.md +1 -1
  77. package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
  78. package/dist/templates/link-digest/agent.eval.test.ts +195 -0
  79. package/dist/templates/link-digest/agent.test.ts +24 -19
  80. package/dist/templates/link-digest/client.tsx +47 -61
  81. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  82. package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
  83. package/dist/templates/night-owl/agent.eval.test.ts +272 -0
  84. package/dist/templates/night-owl/agent.test.ts +70 -19
  85. package/dist/templates/night-owl/agent.ts +5 -0
  86. package/dist/templates/night-owl/client.tsx +56 -56
  87. package/dist/templates/night-owl/shared.ts +24 -0
  88. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  89. package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
  90. package/dist/templates/personal-finance/system-prompt.md +4 -0
  91. package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
  92. package/dist/templates/pipeline-simple/agent.ts +2 -2
  93. package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
  94. package/dist/templates/pizza-ordering/agent.test.ts +42 -27
  95. package/dist/templates/pizza-ordering/client.tsx +9 -26
  96. package/dist/templates/pizza-ordering/shared.ts +20 -1
  97. package/dist/templates/pizza-ordering/system-prompt.md +6 -0
  98. package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
  99. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  100. package/dist/templates/plan-and-execute/agent.ts +1 -1
  101. package/dist/templates/plan-and-execute/client.tsx +12 -15
  102. package/dist/templates/plan-and-execute/shared.ts +71 -2
  103. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  104. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  105. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  106. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  107. package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
  108. package/dist/templates/podcast-digest/agent.test.ts +717 -0
  109. package/dist/templates/podcast-digest/agent.ts +139 -0
  110. package/dist/templates/podcast-digest/client.tsx +154 -0
  111. package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
  112. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  113. package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
  114. package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
  115. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  116. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  117. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  118. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  119. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  120. package/dist/templates/redline/agent.eval.test.ts +209 -0
  121. package/dist/templates/redline/agent.test.ts +17 -15
  122. package/dist/templates/redline/client.tsx +12 -12
  123. package/dist/templates/redline/workflows/redline.ts +19 -31
  124. package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
  125. package/dist/templates/research-workflow/agent.test.ts +60 -59
  126. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  127. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  128. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  129. package/dist/templates/retail/address.ts +87 -1
  130. package/dist/templates/retail/agent.eval.test.ts +314 -0
  131. package/dist/templates/retail/agent.test.ts +195 -112
  132. package/dist/templates/retail/agent.ts +35 -1
  133. package/dist/templates/retail/cancel.ts +116 -0
  134. package/dist/templates/retail/client.tsx +265 -117
  135. package/dist/templates/retail/payment.ts +107 -0
  136. package/dist/templates/retail/pending.test.ts +196 -0
  137. package/dist/templates/retail/pending.ts +137 -0
  138. package/dist/templates/retail/registry.test.ts +150 -7
  139. package/dist/templates/retail/returns.ts +102 -0
  140. package/dist/templates/retail/shared.test.ts +42 -0
  141. package/dist/templates/retail/shared.ts +35 -0
  142. package/dist/templates/retail/store.test.ts +84 -15
  143. package/dist/templates/retail/store.ts +231 -48
  144. package/dist/templates/retail/swap.test.ts +4 -4
  145. package/dist/templates/retail/swap.ts +248 -15
  146. package/dist/templates/retail/system-prompt.md +69 -28
  147. package/dist/templates/retail/tools/cancel_change.ts +33 -0
  148. package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
  149. package/dist/templates/retail/tools/confirm_change.ts +42 -0
  150. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
  151. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  152. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  153. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  154. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  155. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  156. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  157. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  158. package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
  159. package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
  160. package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
  161. package/dist/templates/retail/tools/modify_user_address.ts +13 -16
  162. package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
  163. package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
  164. package/dist/templates/simple/agent.eval.test.ts +54 -0
  165. package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
  166. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  167. package/dist/templates/solo-rpg/client.tsx +38 -37
  168. package/dist/templates/solo-rpg/shared.ts +145 -19
  169. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  170. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  171. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  172. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  173. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  174. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  175. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  176. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  177. package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
  178. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  179. package/dist/templates/spoken-summary/agent.ts +30 -17
  180. package/dist/templates/spoken-summary/client.tsx +10 -50
  181. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  182. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  183. package/dist/templates/support-line/agent.eval.test.ts +273 -0
  184. package/dist/templates/support-line/agent.test.ts +11 -16
  185. package/dist/templates/support-line/agent.ts +1 -1
  186. package/dist/templates/support-line/client.tsx +9 -9
  187. package/dist/templates/support-line/nodes.ts +100 -0
  188. package/dist/templates/support-line/procedure.ts +407 -0
  189. package/dist/templates/support-line/system-prompt.md +2 -1
  190. package/dist/templates/support-line/tools/answer_question.ts +66 -4
  191. package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  193. package/dist/templates/transcription-workflow/agent.ts +1 -1
  194. package/dist/templates/transcription-workflow/client.tsx +17 -42
  195. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  196. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  197. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  198. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  199. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  200. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  201. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  202. package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
  203. package/dist/templates/travel-concierge/agent.test.ts +174 -43
  204. package/dist/templates/travel-concierge/agent.ts +18 -9
  205. package/dist/templates/travel-concierge/client.tsx +11 -23
  206. package/dist/templates/travel-concierge/routing.ts +34 -15
  207. package/dist/templates/travel-concierge/shared.ts +180 -12
  208. package/dist/templates/travel-concierge/system-prompt.md +7 -1
  209. package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
  210. package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
  211. package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
  212. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
  213. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
  214. package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
  215. package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
  216. package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
  217. package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
  218. package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
  219. package/dist/templates/web-researcher/system-prompt.md +15 -4
  220. package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
  221. package/dist/test.d.ts +29 -5
  222. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
  223. package/dist/worker-bundler.mjs +1 -1
  224. package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
  225. package/dist/workflow-bundler.d.ts +6 -1
  226. package/dist/workflow.d.ts +1 -1
  227. package/package.json +5 -4
  228. package/dist/eject-31gjtaHF.mjs +0 -116
  229. package/dist/eject.d.ts +0 -60
  230. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  231. package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
  232. package/dist/templates/support-line/graph.ts +0 -224
  233. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  234. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -27,16 +27,21 @@
27
27
  * runs a real one.
28
28
  */
29
29
 
30
- import type { ToolContext, WorkflowClient, WorkflowRunSnapshot } from "@alexkroman1/aai";
30
+ import type { WorkflowClient } from "@alexkroman1/aai";
31
31
  import {
32
- createProgressStream,
33
32
  createRunSnapshot,
34
- createStubWorkflows,
35
33
  createToolContext,
36
- runTool,
34
+ parseSchemaInput,
35
+ schemaInputIssues,
36
+ toolRunner,
37
37
  withDiscoveredTools,
38
38
  } from "@alexkroman1/aai/testing";
39
- import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
39
+ import {
40
+ installStubStepFetch,
41
+ mockWorkflows,
42
+ installStubGateway as stubGateway,
43
+ } from "@alexkroman1/aai/testing/vitest";
44
+ import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
40
45
  import { beforeEach, describe, expect, test, vi } from "vitest";
41
46
  import { createHook, type Hook, sleep } from "workflow";
42
47
  import authoredAgent from "./agent.ts";
@@ -80,24 +85,27 @@ const agentDef = withDiscoveredTools(
80
85
  import.meta.glob("./tools/*.ts", { eager: true }),
81
86
  );
82
87
 
83
- /** Every tool here is driven through the agent's own table, by the name the model calls. */
84
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext): Promise<unknown> =>
85
- runTool(agentDef, name, args, ctx);
88
+ /**
89
+ * Every tool here is driven through the agent's own table, by the name the model
90
+ * calls.
91
+ *
92
+ * The second parameter is args-or-context, which is `runTool`'s own shape: four
93
+ * of this desk's five tools take no arguments, and the `{}` those calls were
94
+ * obliged to pass sat between the two values a reader cares about.
95
+ */
96
+ const run = toolRunner(agentDef);
86
97
 
87
- /** A `ctx.workflows` that records `start` and answers `find` from a fixture. */
98
+ /**
99
+ * A `ctx.workflows` that records `start` and answers the lookups from a fixture.
100
+ *
101
+ * `mockWorkflows` (`@alexkroman1/aai/testing/vitest`) is the whole thing — a
102
+ * `vi.fn` per method over one `runs` list, with `stream`/`streamTail` left
103
+ * rejecting because `recap_progress` reads progress through `lastLine` and
104
+ * composing those two by hand is the hazard `lastLine` exists to remove. What
105
+ * is local is only which workflow this desk declares.
106
+ */
88
107
  function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
89
- return createStubWorkflows({
90
- start: vi.fn(async () => "wrun_stub"),
91
- get: vi.fn(async () => runs[0]),
92
- find: vi.fn(async () => runs),
93
- recent: vi.fn(async () => runs),
94
- cancel: vi.fn(async () => true),
95
- wakeUp: vi.fn(async () => 0),
96
- // A tail of 0 means "one line written", which is the case the tools read.
97
- streamTail: vi.fn(async () => 0),
98
- stream: vi.fn(async () => createProgressStream([])),
99
- listing: () => [{ name: "recap" }],
100
- });
108
+ return mockWorkflows({ runs, names: ["recap"] });
101
109
  }
102
110
 
103
111
  /** A finished recap, as the workflow's output reaches the tools. */
@@ -122,13 +130,19 @@ describe("the agent declares its workflow", () => {
122
130
  });
123
131
 
124
132
  test("with an input schema, so a bad URL fails at the call site", async () => {
125
- const ok = await recap.input?.["~standard"].validate({
133
+ // `parseSchemaInput` / `schemaInputIssues` rather than a reach through
134
+ // `["~standard"].validate`: that is the vendor WIRE contract, and whether it
135
+ // answers synchronously or with a promise is the vendor's business — a
136
+ // missing `await` there leaves `.issues` undefined and the refusing half
137
+ // passes for the wrong reason.
138
+ const parsed = await parseSchemaInput(recap.input, {
126
139
  url: "https://example.com/a.mp3",
127
140
  requestedBy: "s",
128
141
  });
129
- expect(ok?.issues).toBeUndefined();
130
- const bad = await recap.input?.["~standard"].validate({ url: "not a url", requestedBy: "s" });
131
- expect(bad?.issues).toBeDefined();
142
+ expect(parsed).toMatchObject({ url: "https://example.com/a.mp3" });
143
+ expect(
144
+ await schemaInputIssues(recap.input, { url: "not a url", requestedBy: "s" }),
145
+ ).toBeDefined();
132
146
  });
133
147
 
134
148
  test("and names the credential its steps read, so a deploy checks for it", () => {
@@ -156,7 +170,7 @@ describe("request_recap", () => {
156
170
  test("starts a run keyed by the session, so a later turn can find it", async () => {
157
171
  const workflows = stubWorkflows();
158
172
  const ctx = createToolContext({ workflows });
159
- const result = await run("request_recap", {}, ctx);
173
+ const result = await run("request_recap", ctx);
160
174
 
161
175
  expect(workflows.start).toHaveBeenCalledWith(
162
176
  recap,
@@ -170,7 +184,7 @@ describe("request_recap", () => {
170
184
 
171
185
  test("passes the definition rather than its name", async () => {
172
186
  const workflows = stubWorkflows();
173
- await run("request_recap", {}, createToolContext({ workflows }));
187
+ await run("request_recap", createToolContext({ workflows }));
174
188
  // The def overload is what types the input and turns a rename into a compile
175
189
  // error; a string would still work at runtime and lose both.
176
190
  expect(vi.mocked(workflows.start).mock.calls[0]?.[0]).toBe(recap);
@@ -193,7 +207,7 @@ describe("request_recap", () => {
193
207
  // failure it prevents is not tidiness: a caller who asks twice would
194
208
  // otherwise pay for the same recording being transcribed twice.
195
209
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
196
- const result = await run("request_recap", {}, createToolContext({ workflows }));
210
+ const result = await run("request_recap", createToolContext({ workflows }));
197
211
  expect(result).toMatchObject({ started: false, runId: "wrun_1" });
198
212
  expect(workflows.start).not.toHaveBeenCalled();
199
213
  });
@@ -202,7 +216,7 @@ describe("request_recap", () => {
202
216
  const workflows = stubWorkflows([
203
217
  createRunSnapshot({ workflow: "recap", status: "completed", output: finishedOutput() }),
204
218
  ]);
205
- const result = await run("request_recap", {}, createToolContext({ workflows }));
219
+ const result = await run("request_recap", createToolContext({ workflows }));
206
220
  expect(result).toMatchObject({ started: true });
207
221
  expect(workflows.start).toHaveBeenCalledTimes(1);
208
222
  });
@@ -211,7 +225,7 @@ describe("request_recap", () => {
211
225
  describe("recap_status", () => {
212
226
  test("says nothing was started when the key has no runs", async () => {
213
227
  const ctx = createToolContext({ workflows: stubWorkflows([]) });
214
- const result = await run("recap_status", {}, ctx);
228
+ const result = await run("recap_status", ctx);
215
229
  expect(result).toMatchObject({ runs: [], note: "Nothing started yet." });
216
230
  });
217
231
 
@@ -221,7 +235,7 @@ describe("recap_status", () => {
221
235
  const workflows = stubWorkflows([
222
236
  createRunSnapshot({ workflow: "recap", status: "completed", output: finishedOutput() }),
223
237
  ]);
224
- const result = (await run("recap_status", {}, createToolContext({ workflows }))) as {
238
+ const result = (await run("recap_status", createToolContext({ workflows }))) as {
225
239
  runs: string[];
226
240
  };
227
241
  expect(result.runs[0]).toContain("air quality");
@@ -239,7 +253,6 @@ describe("recap_status", () => {
239
253
  ];
240
254
  const result = (await run(
241
255
  "recap_status",
242
- {},
243
256
  createToolContext({ workflows: stubWorkflows(runs) }),
244
257
  )) as { runs: string[] };
245
258
  expect(result.runs[0]).toContain("transcript deleted");
@@ -249,7 +262,7 @@ describe("recap_status", () => {
249
262
  const ctx = createToolContext({
250
263
  workflows: stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]),
251
264
  });
252
- const result = (await run("recap_status", {}, ctx)) as { runs: string[] };
265
+ const result = (await run("recap_status", ctx)) as { runs: string[] };
253
266
  expect(result.runs[0]).toContain("Still working");
254
267
  });
255
268
 
@@ -262,7 +275,6 @@ describe("recap_status", () => {
262
275
  ];
263
276
  const result = (await run(
264
277
  "recap_status",
265
- {},
266
278
  createToolContext({ workflows: stubWorkflows(runs) }),
267
279
  )) as { runs: string[] };
268
280
  expect(result.runs[0]).toContain("rolled back");
@@ -272,7 +284,7 @@ describe("recap_status", () => {
272
284
  test("bounds how many past runs it reads aloud", async () => {
273
285
  const workflows = stubWorkflows([]);
274
286
  const ctx = createToolContext({ workflows });
275
- await run("recap_status", {}, ctx);
287
+ await run("recap_status", ctx);
276
288
  // A voice reply cannot be a list of twenty runs.
277
289
  expect(workflows.find).toHaveBeenCalledWith(recap, ctx.sessionId, { limit: 3 });
278
290
  });
@@ -281,28 +293,30 @@ describe("recap_status", () => {
281
293
  describe("recap_progress", () => {
282
294
  test("reads the run's own progress line rather than its status", async () => {
283
295
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
284
- vi.mocked(workflows.stream).mockResolvedValue(createProgressStream(["Transcript processing."]));
285
- const result = await run("recap_progress", {}, createToolContext({ workflows }));
296
+ vi.mocked(workflows.lastLine).mockResolvedValue("Transcript processing.");
297
+ const result = await run("recap_progress", createToolContext({ workflows }));
286
298
  expect(result).toMatchObject({ progress: "Transcript processing." });
287
299
  });
288
300
 
289
301
  test("asks for the LAST line, not the whole log", async () => {
290
302
  // Every poll narrates, so a twenty-minute run's whole log is eighty lines.
303
+ // `lastLine` is the whole request — the bound that keeps an empty channel
304
+ // from hanging belongs to the method, so nothing here composes
305
+ // `streamTail` and `stream`.
291
306
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
292
- vi.mocked(workflows.stream).mockResolvedValue(createProgressStream(["a"]));
293
- await run("recap_progress", {}, createToolContext({ workflows }));
294
- expect(workflows.stream).toHaveBeenCalledWith("wrun_1", { startIndex: -1 });
307
+ vi.mocked(workflows.lastLine).mockResolvedValue("a");
308
+ await run("recap_progress", createToolContext({ workflows }));
309
+ expect(workflows.lastLine).toHaveBeenCalledWith("wrun_1");
295
310
  });
296
311
 
297
- test("a run that has written nothing yet says so WITHOUT opening the stream", async () => {
298
- // Not a shortcut: an empty progress channel is never closed, so reading one
299
- // waits for a line that arrives whenever the next step writesi.e. the
300
- // tool hangs instead of answering.
312
+ test("a run that has written nothing yet says so", async () => {
313
+ // `lastLine` resolves `undefined` for an empty channel, and this is the arm
314
+ // the tool branches on. That an empty channel does not HANGit is never
315
+ // closed, so a stream opened on one waits for a line that may never come —
316
+ // is `lastLine`'s own guarantee now, and `aai`'s to test.
301
317
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
302
- vi.mocked(workflows.streamTail).mockResolvedValue(-1);
303
- const result = await run("recap_progress", {}, createToolContext({ workflows }));
318
+ const result = await run("recap_progress", createToolContext({ workflows }));
304
319
  expect(result).toMatchObject({ note: expect.stringContaining("nothing to report") });
305
- expect(workflows.stream).not.toHaveBeenCalled();
306
320
  });
307
321
  });
308
322
 
@@ -343,7 +357,7 @@ describe("keep_transcript — the signal", () => {
343
357
  describe("cancel_recap", () => {
344
358
  test("cancels the live run", async () => {
345
359
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
346
- const result = await run("cancel_recap", {}, createToolContext({ workflows }));
360
+ const result = await run("cancel_recap", createToolContext({ workflows }));
347
361
  expect(workflows.cancel).toHaveBeenCalledWith("wrun_1");
348
362
  expect(result).toMatchObject({ cancelled: true });
349
363
  });
@@ -354,7 +368,7 @@ describe("cancel_recap", () => {
354
368
  // stops replaying the run, so the compensations never fire. A template that
355
369
  // implied otherwise would be teaching the wrong thing.
356
370
  const workflows = stubWorkflows([createRunSnapshot({ workflow: "recap", status: "running" })]);
357
- const result = (await run("cancel_recap", {}, createToolContext({ workflows }))) as {
371
+ const result = (await run("cancel_recap", createToolContext({ workflows }))) as {
358
372
  note: string;
359
373
  };
360
374
  expect(result.note).toContain("left behind");
@@ -365,13 +379,13 @@ describe("cancel_recap", () => {
365
379
  createRunSnapshot({ workflow: "recap", status: "completed", output: finishedOutput() }),
366
380
  ]);
367
381
  vi.mocked(workflows.cancel).mockResolvedValue(false);
368
- const result = await run("cancel_recap", {}, createToolContext({ workflows }));
382
+ const result = await run("cancel_recap", createToolContext({ workflows }));
369
383
  expect(result).toMatchObject({ cancelled: false, note: "That one had already finished." });
370
384
  });
371
385
 
372
386
  test("says nothing was started when the key has no runs", async () => {
373
387
  const workflows = stubWorkflows([]);
374
- const result = await run("cancel_recap", {}, createToolContext({ workflows }));
388
+ const result = await run("cancel_recap", createToolContext({ workflows }));
375
389
  expect(result).toMatchObject({ cancelled: false, note: "Nothing started yet." });
376
390
  expect(workflows.cancel).not.toHaveBeenCalled();
377
391
  });
@@ -379,20 +393,22 @@ describe("cancel_recap", () => {
379
393
 
380
394
  // ---- The steps --------------------------------------------------------------
381
395
 
382
- /** A provider answering `body` with `status`, recording what it was asked. */
396
+ /**
397
+ * A provider answering `body` with `status`, recording what it was asked.
398
+ *
399
+ * Published into `stepFetch`'s OWN slot, not over `globalThis.fetch`. Every
400
+ * request in this file goes through `stepFetch` — `request()` and
401
+ * `discardTranscript` reach it directly, `stepTranscribeSubmitClassified`
402
+ * through the SDK — and `step-fetch.ts` falls back to `globalThis.fetch` only
403
+ * when nothing is published. A global stub therefore passed while exercising a
404
+ * path production never takes; every sibling template already stubs the slot,
405
+ * and `link-digest/agent.test.ts` states the rule this one used to break.
406
+ *
407
+ * `installStubStepFetch` unpublishes on `onTestFinished`, so there is no restore
408
+ * registry here and a stub cannot reach the next file.
409
+ */
383
410
  function stubProvider(body: unknown, status = 200) {
384
- const calls: { url: string; init: RequestInit }[] = [];
385
- vi.stubGlobal(
386
- "fetch",
387
- vi.fn(async (url: string, init: RequestInit = {}) => {
388
- calls.push({ url, init });
389
- return new Response(typeof body === "string" ? body : JSON.stringify(body), {
390
- status,
391
- headers: { "Content-Type": "application/json" },
392
- });
393
- }),
394
- );
395
- return calls;
411
+ return installStubStepFetch(() => ({ status, body })).calls;
396
412
  }
397
413
 
398
414
  describe("submitRecording", () => {
@@ -407,10 +423,10 @@ describe("submitRecording", () => {
407
423
  expect(await submitRecording("https://example.com/a.mp3")).toEqual({ id: "t_1" });
408
424
 
409
425
  const call = calls[0];
410
- expect(call?.init.method).toBe("POST");
426
+ expect(call?.method).toBe("POST");
411
427
  // `speaker_labels` is this desk's own request, carried through the SDK's
412
428
  // `params` passthrough; the model field is the SDK's and is PLURAL.
413
- expect(JSON.parse(String(call?.init.body))).toMatchObject({
429
+ expect(JSON.parse(String(call?.body))).toMatchObject({
414
430
  audio_url: "https://example.com/a.mp3",
415
431
  speaker_labels: true,
416
432
  });
@@ -418,8 +434,7 @@ describe("submitRecording", () => {
418
434
  // OpenAI-compatible LLM gateway `summarize` calls. The SDK spells the
419
435
  // header `Authorization`; HTTP header names are case-insensitive, so the
420
436
  // lookup is too rather than pinning one casing.
421
- const headers = call?.init.headers as Record<string, string> | undefined;
422
- const auth = Object.entries(headers ?? {}).find(
437
+ const auth = Object.entries(call?.headers ?? {}).find(
423
438
  ([name]) => name.toLowerCase() === "authorization",
424
439
  );
425
440
  expect(auth?.[1]).toBe("sk-test");
@@ -488,7 +503,7 @@ describe("discardTranscript — the compensation", () => {
488
503
  test("deletes the transcript this run created", async () => {
489
504
  const calls = stubProvider({ id: "t_1" });
490
505
  await expect(discardTranscript("t_1")).resolves.toBeUndefined();
491
- expect(calls[0]?.init.method).toBe("DELETE");
506
+ expect(calls[0]?.method).toBe("DELETE");
492
507
  expect(calls[0]?.url).toContain("/t_1");
493
508
  });
494
509
 
@@ -580,19 +595,11 @@ describe("awaitTranscript — the polling port", () => {
580
595
 
581
596
  /** A provider whose status endpoint answers `statuses` in order. */
582
597
  function stubStatuses(statuses: readonly Record<string, unknown>[]) {
583
- let call = 0;
584
- vi.stubGlobal(
585
- "fetch",
586
- vi.fn(async () => {
587
- const body = statuses[Math.min(call, statuses.length - 1)];
588
- call += 1;
589
- return new Response(JSON.stringify(body), {
590
- status: 200,
591
- headers: { "Content-Type": "application/json" },
592
- });
593
- }),
594
- );
595
- return () => call;
598
+ // Into `stepFetch`'s slot, for the reason `stubProvider` above gives.
599
+ const stub = installStubStepFetch(() => ({
600
+ body: statuses[Math.min(stub.calls.length - 1, statuses.length - 1)],
601
+ }));
602
+ return () => stub.calls.length;
596
603
  }
597
604
 
598
605
  test("keeps polling while the job is queued or processing", async () => {
@@ -665,15 +672,14 @@ describe("askWhetherToKeep — the expense port", () => {
665
672
 
666
673
  test("keeps the transcript when the caller says to, and deletes nothing", async () => {
667
674
  vi.mocked(createHook).mockReturnValue(hookAnswering({ keep: true }));
668
- const fetchSpy = vi.fn();
669
- vi.stubGlobal("fetch", fetchSpy);
675
+ const provider = installStubStepFetch();
670
676
 
671
677
  const compensations = [{ label: "transcript t_1", undo: async () => undefined }];
672
678
  expect(await askWhetherToKeep("s_1", "t_1", compensations)).toEqual({
673
679
  kept: true,
674
680
  answered: true,
675
681
  });
676
- expect(fetchSpy).not.toHaveBeenCalled();
682
+ expect(provider.calls).toEqual([]);
677
683
  // The undo stays on the stack: a later failure still has something to reverse.
678
684
  expect(compensations).toHaveLength(1);
679
685
  });
@@ -687,7 +693,7 @@ describe("askWhetherToKeep — the expense port", () => {
687
693
  kept: false,
688
694
  answered: true,
689
695
  });
690
- expect(calls[0]?.init.method).toBe("DELETE");
696
+ expect(calls[0]?.method).toBe("DELETE");
691
697
  // Leaving it would be harmless — the undo tolerates a 404 — and would still
692
698
  // narrate an unwind that reverses something already gone.
693
699
  expect(compensations).toHaveLength(0);
@@ -700,7 +706,7 @@ describe("askWhetherToKeep — the expense port", () => {
700
706
  const calls = stubProvider({ id: "t_1" });
701
707
 
702
708
  expect(await askWhetherToKeep("s_1", "t_1", [])).toEqual({ kept: false, answered: false });
703
- expect(calls[0]?.init.method).toBe("DELETE");
709
+ expect(calls[0]?.method).toBe("DELETE");
704
710
  });
705
711
 
706
712
  test("claims the token BEFORE the caller is asked to answer it", async () => {
@@ -711,13 +717,10 @@ describe("askWhetherToKeep — the expense port", () => {
711
717
  vi.mocked(createHook).mockReturnValue(
712
718
  hookAnswering({ keep: true }, () => order.push("claimed")),
713
719
  );
714
- vi.stubGlobal(
715
- "fetch",
716
- vi.fn(async () => {
717
- order.push("asked");
718
- return new Response("{}", { status: 200 });
719
- }),
720
- );
720
+ installStubStepFetch(() => {
721
+ order.push("asked");
722
+ return { body: {} };
723
+ });
721
724
 
722
725
  await askWhetherToKeep("s_1", "t_1", []);
723
726
  expect(order[0]).toBe("claimed");
@@ -11,17 +11,14 @@ export default tool({
11
11
  // otherwise nothing to say — and this run has real news in between,
12
12
  // since every poll narrates.
13
13
  //
14
- // `streamTail` FIRST, and not as an optimization: a progress channel is
15
- // never closed — no step knows it is the last one — so reading a stream
16
- // with nothing in it waits forever rather than ending. `-1` is "nothing
17
- // written yet", and it is the only safe way to learn that.
18
- if ((await ctx.workflows.streamTail(latest.runId)) < 0) {
19
- return { note: "Submitted, nothing to report yet." };
20
- }
21
- // A negative `startIndex` reads from the END, which is what a voice
22
- // reply wants — the last line, not a recital of the whole log.
23
- const stream = await ctx.workflows.stream(latest.runId, { startIndex: -1 });
24
- for await (const line of stream) return { progress: String(line) };
25
- return { note: "Submitted, nothing to report yet." };
14
+ // `lastLine` rather than `streamTail` + `stream` composed here: a progress
15
+ // channel is never closed — no step knows it is the last one — so a stream
16
+ // opened on a run that has written nothing waits forever, which down a phone
17
+ // is a turn that stops with no error and nothing in a log. The bound that
18
+ // prevents it belongs to the method now, and `undefined` is "nothing yet".
19
+ const line = await ctx.workflows.lastLine(latest.runId);
20
+ return line === undefined
21
+ ? { note: "Submitted, nothing to report yet." }
22
+ : { progress: String(line) };
26
23
  },
27
24
  });
@@ -1,9 +1,5 @@
1
- import {
2
- isTerminal,
3
- tool,
4
- type WorkflowOutputOf,
5
- type WorkflowRunSnapshot,
6
- } from "@alexkroman1/aai";
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
7
3
  import { recap } from "../shared.ts";
8
4
 
9
5
  /** How many past runs the status tool will look at. Newest first. */
@@ -14,10 +10,13 @@ const RECENT_RUNS = 3;
14
10
  *
15
11
  * This is the QUERY, and `isTerminal` is what makes it typed: it narrows to the
16
12
  * three finished statuses, which is what puts `run.output` and `run.error`
17
- * within reach without a cast. `WorkflowOutputOf` names the output type from the
18
- * declaration, so this signature never reaches past it into the body.
13
+ * within reach without a cast. `WorkflowRunOf` is the snapshot with that output
14
+ * already named from the declaration the
15
+ * `WorkflowRunSnapshot<WorkflowOutputOf<typeof recap>>` this file used to
16
+ * compose by hand, for a three-name import — so the signature never reaches past
17
+ * the declaration into the body, and it is still the discriminated union.
19
18
  */
20
- function describeRun(run: WorkflowRunSnapshot<WorkflowOutputOf<typeof recap>>): string {
19
+ function describeRun(run: WorkflowRunOf<typeof recap>): string {
21
20
  if (!isTerminal(run)) return "Still working on that one.";
22
21
  switch (run.status) {
23
22
  case "completed": {
@@ -1,4 +1,5 @@
1
- import { isTerminal, tool } from "@alexkroman1/aai";
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { isTerminal } from "@alexkroman1/aai/workflow-api";
2
3
  import { z } from "zod";
3
4
  import { recap, SAMPLE_RECORDING } from "../shared.ts";
4
5
 
@@ -55,7 +55,7 @@
55
55
  * Everything the desk claims to do. `submitRecording`, `checkTranscript` and
56
56
  * `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
57
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
58
+ * `stepGenerateJsonClassified`. The BATCH API is what makes the polling port honest: it
59
59
  * answers with a job id in milliseconds and finishes minutes later, so the wait
60
60
  * is the provider's, not a `setTimeout` this template chose. (Its sibling
61
61
  * `transcription-workflow` takes the other endpoint — the sync one, which answers in
@@ -67,17 +67,14 @@
67
67
  * just your shell.
68
68
  */
69
69
 
70
- import { throwStepError, toStepError } from "@alexkroman1/aai/step-errors";
70
+ import { report, requireStepEnv, stepFetch } from "@alexkroman1/aai/step";
71
71
  import {
72
- errorMessage,
73
- isRecord,
74
- omitUndefined,
75
- report,
76
- requireStepEnv,
77
- stepFetch,
78
- stepGenerateJson,
79
- stepTranscribeSubmit,
80
- } from "@alexkroman1/aai/utils";
72
+ stepFetchOk,
73
+ stepGenerateJsonClassified,
74
+ stepTranscribeSubmitClassified,
75
+ toStepError,
76
+ } from "@alexkroman1/aai/step-errors";
77
+ import { errorMessage, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
81
78
  import { createHook, FatalError, sleep } from "workflow";
82
79
  import { z } from "zod";
83
80
  import { retentionToken } from "./tokens.ts";
@@ -151,7 +148,7 @@ const POINTS = 3;
151
148
  /**
152
149
  * The shape the model must answer in.
153
150
  *
154
- * `stepGenerateJson` validates against this and throws PLAINLY when the reply
151
+ * `stepGenerateJsonClassified` validates against this and throws PLAINLY when the reply
155
152
  * misses, which is the retry policy in one distinction: a model that answered in
156
153
  * prose may answer correctly next time, where a 401 will not. `spoken` is the
157
154
  * field this template exists for — without it the announced turn has nothing to
@@ -257,7 +254,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }) {
257
254
  * to completion in a worker, where the body is what may suspend. Splitting it
258
255
  * out keeps `recapFlow` readable and costs nothing, since the WDK transform
259
256
  * rewrites a step's DECLARATION rather than its call sites, so a step called
260
- * from a helper is still a real step (`mapInBatches` rests on the same
257
+ * from a helper is still a real step (`mapConcurrent` rests on the same
261
258
  * property).
262
259
  *
263
260
  * The loop is deterministic despite looking like it is not: every branch turns
@@ -359,13 +356,14 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
359
356
 
360
357
  await report(`Submitting ${new URL(url).hostname} for transcription…`);
361
358
 
362
- // `stepTranscribeSubmit` owns the endpoint, the raw-key auth, the PLURAL
363
- // `speech_models` field and the failure classification. `speaker_labels` is
364
- // this desk's own request, which is what `params` is for — the async API's
359
+ // `stepTranscribeSubmitClassified` owns the endpoint, the raw-key auth, the
360
+ // PLURAL `speech_models` field and the failure classification the
361
+ // `Classified` suffix being that last part: it is `stepTranscribeSubmit` with
362
+ // `throwStepError` already applied, so a provider refusal stays terminal and a
363
+ // rate limit waits out the delay the provider itself named. `speaker_labels`
364
+ // is this desk's own request, which is what `params` is for — the async API's
365
365
  // surface is large and the SDK deliberately does not mirror it.
366
- return await stepTranscribeSubmit(url, { params: { speaker_labels: true } }).catch(
367
- throwStepError,
368
- );
366
+ return await stepTranscribeSubmitClassified(url, { params: { speaker_labels: true } });
369
367
  }
370
368
 
371
369
  /**
@@ -467,19 +465,20 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
467
465
  throw new FatalError("That recording came back with no speech in it.");
468
466
  }
469
467
 
470
- // `stepGenerateJson` unwraps the fence a model puts around JSON however firmly
471
- // it is told not to, parses it, and validates it — all four things this step
472
- // used to re-derive. `throwStepError` is what makes a terminal gateway failure
473
- // (a bad key, a rejected request) stop rather than burn the remaining
474
- // attempts, where a reply that missed the SHAPE throws plainly and retries.
475
- const parsed = await stepGenerateJson(text, {
468
+ // `stepGenerateJsonClassified` unwraps the fence a model puts around JSON
469
+ // however firmly it is told not to, parses it, and validates it — all four
470
+ // things this step used to re-derive. The `Classified` half is what makes a
471
+ // terminal gateway failure (a bad key, a rejected request) stop rather than
472
+ // burn the remaining attempts, where a reply that missed the SHAPE throws
473
+ // plainly and retries.
474
+ const parsed = await stepGenerateJsonClassified(text, {
476
475
  schema: RecapReply,
477
476
  system:
478
477
  "You write up recordings for someone who will hear the result on a phone call. " +
479
478
  `Reply with JSON only: {"headline": string, "points": string[], "spoken": string}. ` +
480
479
  `Give exactly ${POINTS} points. "spoken" is ONE sentence, under 30 words, ` +
481
480
  "written to be read aloud. No markdown fence, no preamble.",
482
- }).catch(throwStepError);
481
+ });
483
482
 
484
483
  return {
485
484
  url,
@@ -519,27 +518,24 @@ async function request(
519
518
  url: string,
520
519
  init: { method?: string; body?: string } = {},
521
520
  ): Promise<Response> {
522
- // `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent runs (and
523
- // this workflow POLLS, so one run is many requests) get a socket each rather
524
- // than N streams on one connection — and a connection failure arrives as a
525
- // `StepTransportError` naming its cause instead of a bare
521
+ // Through `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
522
+ // runs (and this workflow POLLS, so one run is many requests) get a socket
523
+ // each rather than N streams on one connection — and a connection failure
524
+ // arrives as a `StepTransportError` naming its cause instead of a bare
526
525
  // `TypeError: fetch failed`, which for a template whose whole subject is
527
526
  // durability is the difference between a diagnosable resume and a mystery.
528
527
  // `sdk/step-fetch.ts` carries the measurements.
529
- const response = await stepFetch(url, {
528
+ // `stepFetchOk` makes the three-way retry decision: a 401 or a 400 answers the
529
+ // same way on the fourth attempt and burns the step, a 429 or a 5xx is what
530
+ // retries are for, and a `Retry-After` the provider named is waited out rather
531
+ // than replaced by the DevKit's one-second default — which matters here more
532
+ // than usual, because a fan-out of segments hits a rate limit together. The
533
+ // DELETE below stays on plain `stepFetch`, because there a 404 is a SUCCESS.
534
+ return await stepFetchOk(url, {
530
535
  ...init,
531
536
  headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
532
537
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
533
538
  });
534
- // The three-way retry decision, made by the SDK: a 401 or a 400 answers the
535
- // same way on the fourth attempt and burns the step, a 429 or a 5xx is what
536
- // retries are for, and a `Retry-After` the provider named is waited out rather
537
- // than replaced by the DevKit's one-second default — which matters here more
538
- // than usual, because a fan-out of segments hits a rate limit together.
539
- if (!response.ok) {
540
- throw toStepError(response, `${init.method ?? "GET"} ${url} failed: HTTP ${response.status}`);
541
- }
542
- return response;
543
539
  }
544
540
 
545
541
  /** A string field of a JSON body, when it really is one. */