@alexkroman1/aai-cli 13.3.0 → 14.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 (134) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
  4. package/dist/_cli-common.d.ts +45 -6
  5. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  6. package/dist/_dev-env.d.ts +1 -1
  7. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
  8. package/dist/_dev-vite-config.d.ts +3 -3
  9. package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
  10. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  11. package/dist/_output.d.ts +10 -9
  12. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  13. package/dist/_resource-commands.d.ts +9 -9
  14. package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
  15. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  16. package/dist/_slug-api.d.ts +19 -0
  17. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  18. package/dist/_studio-commands.d.ts +13 -13
  19. package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
  20. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  21. package/dist/_typecheck-gate.d.ts +3 -1
  22. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  23. package/dist/_utils.d.ts +35 -0
  24. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  25. package/dist/_workflow-determinism.d.ts +3 -3
  26. package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
  27. package/dist/build.d.ts +5 -0
  28. package/dist/cli.d.ts +1 -2
  29. package/dist/cli.mjs +117 -135
  30. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
  31. package/dist/client-bundler.mjs +1 -1
  32. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  33. package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
  34. package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
  35. package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
  36. package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
  37. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  38. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  39. package/dist/project-config.mjs +1 -1
  40. package/dist/scaffold/CLAUDE.md +84 -56
  41. package/dist/scaffold/package.json +4 -4
  42. package/dist/scaffold/server.mjs +129 -25
  43. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  44. package/dist/secret.d.ts +8 -6
  45. package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
  46. package/dist/templates/call-audit/agent.test.ts +11 -6
  47. package/dist/templates/call-audit/agent.ts +2 -2
  48. package/dist/templates/call-audit/client.tsx +27 -42
  49. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  50. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  51. package/dist/templates/call-audit/workflows/media.ts +12 -37
  52. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  53. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  54. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  55. package/dist/templates/dispatch-center/client.tsx +53 -61
  56. package/dist/templates/dispatch-center/shared.ts +0 -8
  57. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  58. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  59. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  60. package/dist/templates/infocom-adventure/client.tsx +40 -36
  61. package/dist/templates/link-digest/agent.test.ts +3 -3
  62. package/dist/templates/link-digest/agent.ts +2 -2
  63. package/dist/templates/link-digest/client.tsx +27 -17
  64. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  65. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  66. package/dist/templates/night-owl/client.tsx +8 -2
  67. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  68. package/dist/templates/pizza-ordering/client.tsx +5 -4
  69. package/dist/templates/pizza-ordering/shared.ts +3 -8
  70. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  71. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  72. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  73. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  74. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  75. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  76. package/dist/templates/plan-and-execute/client.tsx +2 -2
  77. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  78. package/dist/templates/podcast-digest/client.tsx +16 -22
  79. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  80. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  81. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  82. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  83. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  84. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  85. package/dist/templates/redline/agent.eval.test.ts +5 -8
  86. package/dist/templates/redline/agent.test.ts +1 -1
  87. package/dist/templates/redline/client.tsx +36 -34
  88. package/dist/templates/redline/workflows/redline.ts +17 -17
  89. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  90. package/dist/templates/research-workflow/agent.test.ts +10 -7
  91. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  92. package/dist/templates/retail/agent.test.ts +17 -15
  93. package/dist/templates/retail/cancel.ts +7 -6
  94. package/dist/templates/retail/client.tsx +39 -58
  95. package/dist/templates/retail/payment.ts +4 -3
  96. package/dist/templates/retail/pending.test.ts +19 -19
  97. package/dist/templates/retail/store.test.ts +5 -5
  98. package/dist/templates/retail/swap.ts +8 -11
  99. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  100. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  101. package/dist/templates/solo-rpg/client.tsx +3 -3
  102. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  103. package/dist/templates/spoken-summary/agent.ts +3 -3
  104. package/dist/templates/spoken-summary/client.tsx +23 -30
  105. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  106. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  107. package/dist/templates/support-line/client.tsx +4 -3
  108. package/dist/templates/support-line/prompts.ts +0 -2
  109. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  110. package/dist/templates/transcription-workflow/agent.ts +2 -2
  111. package/dist/templates/transcription-workflow/client.tsx +15 -14
  112. package/dist/templates/transcription-workflow/recover.ts +1 -1
  113. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  114. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  115. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  116. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  117. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  118. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  119. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  120. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  121. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  122. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  123. package/dist/templates/travel-concierge/client.tsx +2 -2
  124. package/dist/templates/travel-concierge/shared.ts +10 -14
  125. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  126. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  127. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  128. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  129. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  130. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  131. package/dist/typecheck.mjs +1 -1
  132. package/dist/worker-bundler.mjs +1 -1
  133. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  134. package/package.json +4 -4
@@ -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
  */
@@ -59,9 +59,11 @@ import "@alexkroman1/aai-ui/styles.css";
59
59
  import { plural } from "@alexkroman1/aai/utils";
60
60
  import type { WorkflowInputOf, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
61
61
  import {
62
+ BulletList,
63
+ Facts,
62
64
  Form,
63
65
  type FormValues,
64
- page,
66
+ mountPage,
65
67
  SubmitButton,
66
68
  TextAreaField,
67
69
  useWorkflowSubmit,
@@ -70,7 +72,6 @@ import {
70
72
  WorkflowProgress,
71
73
  type WorkflowRun,
72
74
  } from "@alexkroman1/aai-ui";
73
- import { useState } from "react";
74
75
  import type { redline } from "./agent.ts";
75
76
 
76
77
  /**
@@ -124,13 +125,10 @@ export function toInput(values: FormValues): WorkflowInputOf<typeof redline> {
124
125
  }
125
126
 
126
127
  function RedlineDesk() {
127
- // Did THIS load start the run? A reload cannot have, and that is the only way
128
- // the page can tell "writing what you just briefed" from "picking up where
129
- // you left off" — the hook reports the run, not who asked for it.
130
- const [startedHere, setStartedHere] = useState(false);
131
128
  // The reload is covered by the hook's own key — see the module doc for why
132
129
  // this desk wants the tab-scoped one it mints rather than a key of its own.
133
- const { submit, run, pending, error, reset } = useWorkflowSubmit<typeof redline>(WORKFLOW);
130
+ const { submit, run, pending, error, reset, startedHere } =
131
+ useWorkflowSubmit<typeof redline>(WORKFLOW);
134
132
 
135
133
  return (
136
134
  <main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
@@ -142,13 +140,7 @@ function RedlineDesk() {
142
140
  </p>
143
141
  </header>
144
142
 
145
- <Form
146
- onSubmit={(values) => {
147
- setStartedHere(true);
148
- return submit(toInput(values));
149
- }}
150
- error={error}
151
- >
143
+ <Form onSubmit={(values) => submit(toInput(values))} error={error}>
152
144
  {/* The scalars: brief, audience, rounds. Declared, not written. */}
153
145
  <WorkflowFields workflow={WORKFLOW} />
154
146
  {/* The array the schema declares and no generic control can render. */}
@@ -177,7 +169,6 @@ function RedlineDesk() {
177
169
  // The recovered run is dismissed as deliberately as one this load
178
170
  // started: `reset()` is not undone by a second lookup (the lookup
179
171
  // is a mount-time act), so Clear really does clear.
180
- setStartedHere(false);
181
172
  reset();
182
173
  }}
183
174
  />
@@ -193,15 +184,16 @@ function Rounds({ rounds }: { rounds: Redline["rounds"] }) {
193
184
  <ol className="flex flex-col gap-3">
194
185
  {rounds.map((entry) => (
195
186
  <li key={entry.round} className="flex flex-col gap-1 border-l pl-4">
196
- <p className="text-xs uppercase tracking-[1.2px] opacity-60">
197
- Round {entry.round} · {entry.critique.score}/10 ·{" "}
198
- {entry.critique.verdict === "ship" ? "ship it" : "revise"}
199
- </p>
200
- <ul className="flex list-disc flex-col gap-1 pl-5 text-sm">
201
- {entry.critique.notes.map((note) => (
202
- <li key={note}>{note}</li>
203
- ))}
204
- </ul>
187
+ <Facts
188
+ size="xs"
189
+ className="uppercase tracking-[1.2px]"
190
+ items={[
191
+ `Round ${entry.round}`,
192
+ `${entry.critique.score}/10`,
193
+ entry.critique.verdict === "ship" ? "ship it" : "revise",
194
+ ]}
195
+ />
196
+ <BulletList items={entry.critique.notes} size="sm" />
205
197
  </li>
206
198
  ))}
207
199
  </ol>
@@ -224,7 +216,7 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
224
216
  {/* The run's own narration — the complement of the status line, which is
225
217
  `running` for a run's whole life, so a one-round redline and a
226
218
  three-round one look identical while they happen. These lines come from
227
- the run itself (`report()` in `workflows/redline.ts`), and they REPLAY,
219
+ the run itself (`stepReport()` in `workflows/redline.ts`), and they REPLAY,
228
220
  so a reload mid-run catches up rather than starting from whatever
229
221
  arrives next. */}
230
222
  <WorkflowProgress runId={run.runId} />
@@ -233,20 +225,30 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
233
225
  without a cast. */}
234
226
  {run.status === "completed" && (
235
227
  <>
236
- <p className="text-xs opacity-60">
237
- {run.output.words} words · {run.output.roundsRun}{" "}
238
- {plural(run.output.roundsRun, "round")} ·{" "}
239
- {/* Which of the two stop conditions ended the loop is the one thing
240
- a reader cannot infer from the round count alone. */}
241
- {run.output.shipped ? "the critic stopped it" : "the round budget stopped it"}
242
- </p>
228
+ {/* Which of the two stop conditions ended the loop is the one thing a
229
+ reader cannot infer from the round count alone, so it is a fact of
230
+ its own rather than something left to the round count. */}
231
+ <Facts
232
+ size="xs"
233
+ items={[
234
+ `${run.output.words} words`,
235
+ `${run.output.roundsRun} ${plural(run.output.roundsRun, "round")}`,
236
+ run.output.shipped ? "the critic stopped it" : "the round budget stopped it",
237
+ ]}
238
+ />
243
239
  <Rounds rounds={run.output.rounds} />
244
240
  <article className="whitespace-pre-wrap text-sm leading-relaxed">
245
241
  {run.output.draft}
246
242
  </article>
247
243
  </>
248
244
  )}
249
- {run.status === "failed" && <p className="text-red-600">{run.error}</p>}
245
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error: this
246
+ is the outcome the reader waited minutes for. */}
247
+ {run.status === "failed" && (
248
+ <p role="alert" className="text-red-600">
249
+ {run.error}
250
+ </p>
251
+ )}
250
252
  </section>
251
253
  );
252
254
  }
@@ -263,4 +265,4 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
263
265
  */
264
266
  const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
265
267
 
266
- page({ name: "Redline", component: RedlineDesk });
268
+ mountPage({ name: "Redline", component: RedlineDesk });
@@ -32,12 +32,12 @@
32
32
  * three functions read more tidily than one.
33
33
  */
34
34
 
35
- import type { WorkflowCtx } from "@alexkroman1/aai";
36
- import { report } from "@alexkroman1/aai/step";
35
+ import type { WorkflowContext } from "@alexkroman1/aai";
36
+ import { stepReport } from "@alexkroman1/aai/step";
37
37
  import {
38
38
  FatalError,
39
- stepGenerateClassified,
40
- stepGenerateJsonClassified,
39
+ stepGenerateJsonOrFail,
40
+ stepGenerateOrFail,
41
41
  } from "@alexkroman1/aai/step-errors";
42
42
  import { countWords } from "@alexkroman1/aai/utils";
43
43
  import { z } from "zod";
@@ -98,7 +98,7 @@ export interface Round {
98
98
  * the page's render model — and `WorkflowOutputOf<typeof redline>` in
99
99
  * `client.tsx` is that type, derived rather than restated.
100
100
  */
101
- export async function redlineFlow(input: RedlineInput, ctx: WorkflowCtx) {
101
+ export async function redlineFlow(input: RedlineInput, ctx: WorkflowContext) {
102
102
  // The three `maxAttempts` below were `maxRetries` properties on the functions
103
103
  // (3, 5, 3 — retries AFTER the first attempt, so 4, 6, 4 in all). The policy
104
104
  // is an argument to the CALL now, which is where it belongs: the same function
@@ -153,12 +153,12 @@ export async function writeDraft(input: RedlineInput): Promise<string> {
153
153
  );
154
154
  }
155
155
 
156
- await report(`Writing the first draft for ${input.audience}.`);
156
+ await stepReport(`Writing the first draft for ${input.audience}.`);
157
157
  // No empty-reply guard here or in `reviseDraft`, and that is not an omission:
158
158
  // `stepGenerate` already refuses an empty completion, as a RETRYABLE
159
159
  // `StepGenerateError` — which is the right answer, and one a hand-written
160
160
  // check would have to re-derive.
161
- const draft = await stepGenerateClassified(briefBlock(input), { system: WRITER_SYSTEM });
161
+ const draft = await stepGenerateOrFail(briefBlock(input), { system: WRITER_SYSTEM });
162
162
  return draft.trim();
163
163
  }
164
164
 
@@ -175,14 +175,14 @@ export async function critiqueDraft(
175
175
  input: RedlineInput,
176
176
  round: number,
177
177
  ): Promise<Critique> {
178
- await report(`Round ${round}: reading it back critically.`);
178
+ await stepReport(`Round ${round}: reading it back critically.`);
179
179
  // `stepGenerateJson` owns the fence, the parse, the non-object case and the
180
180
  // shape — and throws PLAINLY when any of them misses, unlike the fatal one
181
181
  // above: a model that answered with prose may well obey on the next attempt.
182
- const parsed = await stepGenerateJsonClassified(
183
- `${briefBlock(input)}\n\nThe submission:\n${draft}`,
184
- { schema: CritiqueReply, system: CRITIC_SYSTEM },
185
- );
182
+ const parsed = await stepGenerateJsonOrFail(`${briefBlock(input)}\n\nThe submission:\n${draft}`, {
183
+ schema: CritiqueReply,
184
+ system: CRITIC_SYSTEM,
185
+ });
186
186
 
187
187
  const critique: Critique = {
188
188
  verdict: parsed.verdict,
@@ -192,7 +192,7 @@ export async function critiqueDraft(
192
192
  score: clampScore(parsed.score),
193
193
  notes: parsed.notes.slice(0, MAX_NOTES),
194
194
  };
195
- await report(
195
+ await stepReport(
196
196
  critique.verdict === "ship"
197
197
  ? `Round ${round}: the critic would ship it (${critique.score}/10).`
198
198
  : `Round ${round}: ${critique.notes.length} note(s) to address (${critique.score}/10).`,
@@ -207,8 +207,8 @@ export async function reviseDraft(
207
207
  input: RedlineInput,
208
208
  round: number,
209
209
  ): Promise<string> {
210
- await report(`Round ${round}: revising.`);
211
- const revised = await stepGenerateClassified(
210
+ await stepReport(`Round ${round}: revising.`);
211
+ const revised = await stepGenerateOrFail(
212
212
  [
213
213
  briefBlock(input),
214
214
  `Your current draft:\n${draft}`,
@@ -241,8 +241,8 @@ export function clampScore(score: number): number {
241
241
  // There is no local `ask()` any more, and its absence is the point. The SDK
242
242
  // classifies the gateway's failure (`StepGenerateError.retryable`) and stops
243
243
  // there — whether a terminal failure should burn the step's remaining attempts
244
- // is the caller's call — so `stepGenerateClassified` and
245
- // `stepGenerateJsonClassified` (`@alexkroman1/aai/step-errors`) are that call
244
+ // is the caller's call — so `stepGenerateOrFail` and
245
+ // `stepGenerateJsonOrFail` (`@alexkroman1/aai/step-errors`) are that call
246
246
  // made one way: terminal stays terminal, and a rate limit becomes a
247
247
  // `RetryableError` carrying the delay the gateway itself named, which beats
248
248
  // `RetryableError`'s own one-second default. Three templates each wrapped the