@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
@@ -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
@@ -29,7 +29,7 @@
29
29
  // endpointing, barge-in, whether two sentences merged into one turn.
30
30
 
31
31
  import agentDef from "virtual:aai/agent";
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 { type EvalToolCall, type EvalWorkflows, toolResultIn } from "@alexkroman1/aai-runtime/eval";
35
35
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
@@ -132,11 +132,11 @@ type ScriptedSteps = {
132
132
  function scriptSteps(options: { hold?: boolean } = {}): ScriptedSteps {
133
133
  const gate = Promise.withResolvers<void>();
134
134
  const model = stubGatewayRoute(MODEL_SCRIPT);
135
+ // Throwing on an unrecognised request is `routeStepFetch`'s default and is
136
+ // what this file wants: every step here is a model call.
137
+ const route = routeStepFetch([model.route]);
135
138
  const stub = installStubStepFetch(async (request) => {
136
- const answered = model.route(request);
137
- if (answered === undefined) {
138
- throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
139
- }
139
+ const answered = route(request);
140
140
  // `model.calls` has already recorded this one, so a length of 1 IS the first
141
141
  // answer — and holding after the route rather than before it keeps the reply
142
142
  // this returns the one the script owed that call.
@@ -16,7 +16,7 @@
16
16
  * exported async function, so its prompt handling, its parsing and its
17
17
  * `FatalError` guards are all testable without an engine.
18
18
  *
19
- * The BODY is driven here only through `createWorkflowCtx`, which records what
19
+ * The BODY is driven here only through `createWorkflowContext`, which records what
20
20
  * it asked for and replays nothing. That is a choice rather than a limit now:
21
21
  * `runWorkflow` from `@alexkroman1/aai-runtime/testing` will run this body on
22
22
  * the real engine, and `link-digest` is the template that shows it — three
@@ -31,13 +31,16 @@ import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
31
31
  import {
32
32
  createRunSnapshot,
33
33
  createToolContext,
34
- createWorkflowCtx,
34
+ createWorkflowContext,
35
35
  parseSchemaInput,
36
36
  type StubGatewayCall,
37
37
  schemaInputIssues,
38
38
  toolRunner,
39
39
  } from "@alexkroman1/aai/testing";
40
- import { mockWorkflows, installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
40
+ import {
41
+ installStubWorkflows,
42
+ installStubGateway as stubGateway,
43
+ } from "@alexkroman1/aai/testing/vitest";
41
44
  import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
42
45
  import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
43
46
  import { runWorkflow } from "@alexkroman1/aai-runtime/testing";
@@ -89,7 +92,7 @@ const run = toolRunner(agentDef);
89
92
  *
90
93
  * Returned WITHOUT a cast, which is the property worth keeping: a cast would
91
94
  * also stop reporting the day `WorkflowClient` grows a method, and this stub is
92
- * how the template's tools reach the client at all. `mockWorkflows`
95
+ * how the template's tools reach the client at all. `installStubWorkflows`
93
96
  * (`@alexkroman1/aai/testing/vitest`) is what keeps that affordable — a `vi.fn`
94
97
  * per method over one `runs` list, filling in what this desk does not drive, so
95
98
  * the day the client does grow a method only the tests using it change.
@@ -101,7 +104,7 @@ function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
101
104
  // Name only: `WorkflowSummary.description` is optional, so passing this
102
105
  // desk's through would mean handing `description: undefined` to a field that
103
106
  // does not accept it. Nothing here reads the description anyway.
104
- return mockWorkflows({ runs, names: ["research"] });
107
+ return installStubWorkflows({ runs, names: ["research"] });
105
108
  }
106
109
 
107
110
  describe("the agent declares its workflow", () => {
@@ -445,7 +448,7 @@ describe("the steps that research", () => {
445
448
  // could not have said differently.
446
449
  // `planAngles`' result is what the fan-out iterates, so it is supplied
447
450
  // rather than run — the rest of the body needs no page and no model.
448
- const ctx = createWorkflowCtx({
451
+ const ctx = createWorkflowContext({
449
452
  runSteps: false,
450
453
  // Every step the body READS needs a value: with `runSteps: false` nothing
451
454
  // runs, so this is the skeleton of a run rather than a run. That is the
@@ -556,7 +559,7 @@ describe("the steps that research", () => {
556
559
  /**
557
560
  * `researchFlow` itself, on the real replay engine.
558
561
  *
559
- * The block above drives this body through `createWorkflowCtx`, which records
562
+ * The block above drives this body through `createWorkflowContext`, which records
560
563
  * what it ASKED for and replays nothing — right for the retry policy and the
561
564
  * step order, and silent about the desk's actual promise: **answer the caller
562
565
  * now, finish the work later**. `runWorkflow`
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * ## Every stage REPORTS, and the report goes two places
37
37
  *
38
- * `report()` (`@alexkroman1/aai/step`) writes to the run's own stream — which
38
+ * `stepReport()` (`@alexkroman1/aai/step`) writes to the run's own stream — which
39
39
  * `research_progress` reads back down the phone and a page renders — and to the
40
40
  * server log, with the attempt number appended past the first. A pass that is
41
41
  * retrying and one that is working print the same sentences otherwise.
@@ -49,9 +49,9 @@
49
49
  * researcher CONCLUDED, which is exactly what the step returns.
50
50
  */
51
51
 
52
- import type { WorkflowCtx } from "@alexkroman1/aai";
53
- import { mapConcurrent, report } from "@alexkroman1/aai/step";
54
- import { stepGenerateClassified, stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
52
+ import type { WorkflowContext } from "@alexkroman1/aai";
53
+ import { mapConcurrent, stepReport } from "@alexkroman1/aai/step";
54
+ import { stepGenerateJsonOrFail, stepGenerateOrFail } from "@alexkroman1/aai/step-errors";
55
55
  import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
56
56
  import { errorMessage, isToolFailure, plural } from "@alexkroman1/aai/utils";
57
57
  import { z } from "zod";
@@ -107,7 +107,7 @@ export type Source = { title: string; url: string };
107
107
 
108
108
  // ---- What each stage's model call has to come back as ------------------------
109
109
  //
110
- // `stepGenerateJsonClassified` validates against these, so a reply that missed
110
+ // `stepGenerateJsonOrFail` validates against these, so a reply that missed
111
111
  // is a plain throw and therefore a retry — where the hand-rolled `askJson<T>()`
112
112
  // this replaces returned a value the compiler believed and nothing checked. They are
113
113
  // deliberately LENIENT wherever the old hand-written coercion was: a model that
@@ -212,7 +212,7 @@ export type Findings = {
212
212
  */
213
213
  export async function researchFlow(
214
214
  input: { topic: string; requestedBy: string },
215
- ctx: WorkflowCtx,
215
+ ctx: WorkflowContext,
216
216
  ) {
217
217
  const brief = await ctx.step("writeBrief", () => writeBrief(input.topic));
218
218
  const angles = await ctx.step("planAngles", () => planAngles(brief));
@@ -270,11 +270,11 @@ export async function researchFlow(
270
270
  * later model call.
271
271
  */
272
272
  export async function writeBrief(topic: string): Promise<Brief> {
273
- await report(`Working out what "${topic}" is really asking.`);
274
- const parsed = await stepGenerateJsonClassified(
275
- `Research request, as the caller said it: ${topic}`,
276
- { system: BRIEF_SYSTEM, schema: BriefReply },
277
- );
273
+ await stepReport(`Working out what "${topic}" is really asking.`);
274
+ const parsed = await stepGenerateJsonOrFail(`Research request, as the caller said it: ${topic}`, {
275
+ system: BRIEF_SYSTEM,
276
+ schema: BriefReply,
277
+ });
278
278
  return { brief: parsed.brief || topic, criteria: parsed.criteria.slice(0, MAX_ANGLES) };
279
279
  }
280
280
 
@@ -286,7 +286,7 @@ export async function writeBrief(topic: string): Promise<Brief> {
286
286
  * asking the model again and getting a different one.
287
287
  */
288
288
  export async function planAngles(brief: Brief): Promise<string[]> {
289
- const parsed = await stepGenerateJsonClassified(briefText(brief), {
289
+ const parsed = await stepGenerateJsonOrFail(briefText(brief), {
290
290
  system: PLAN_SYSTEM,
291
291
  schema: AnglesReply,
292
292
  });
@@ -294,10 +294,10 @@ export async function planAngles(brief: Brief): Promise<string[]> {
294
294
  if (angles.length === 0) {
295
295
  // Nothing to fan out over is a plan failure, not an empty result: the brief
296
296
  // itself is the one angle that is always available.
297
- await report("No angles came back; researching the brief itself.");
297
+ await stepReport("No angles came back; researching the brief itself.");
298
298
  return [brief.brief];
299
299
  }
300
- await report(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
300
+ await stepReport(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
301
301
  return angles;
302
302
  }
303
303
 
@@ -309,7 +309,7 @@ export async function planAngles(brief: Brief): Promise<string[]> {
309
309
  * at the end, which is where it becomes small enough to journal.
310
310
  */
311
311
  export async function investigate(brief: Brief, angle: string): Promise<Note> {
312
- await report(`Looking into: ${angle}`);
312
+ await stepReport(`Looking into: ${angle}`);
313
313
  const seen: string[] = [];
314
314
  const sources: Source[] = [];
315
315
 
@@ -323,7 +323,7 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
323
323
  continue;
324
324
  }
325
325
  if (action.action === "read" && action.url) {
326
- await report(`Reading ${hostname(action.url)}`);
326
+ await stepReport(`Reading ${hostname(action.url)}`);
327
327
  seen.push(`PAGE ${action.url}\n${await readPage(action.url)}`);
328
328
  continue;
329
329
  }
@@ -335,8 +335,6 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
335
335
  return await compress(angle, seen, sources);
336
336
  }
337
337
 
338
- /** UNUSED-ORPHAN-MARKER Retries beyond the default: the far side is a search engine and a model. */
339
-
340
338
  /**
341
339
  * The supervisor's second look.
342
340
  *
@@ -346,12 +344,12 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
346
344
  */
347
345
  export async function findGaps(brief: Brief, notes: readonly Note[]): Promise<string[]> {
348
346
  if (notes.length === 0) return [];
349
- const parsed = await stepGenerateJsonClassified(
347
+ const parsed = await stepGenerateJsonOrFail(
350
348
  `${briefText(brief)}\n\nWhat came back:\n${notes.map(noteText).join("\n\n")}`,
351
349
  { system: GAPS_SYSTEM, schema: AnglesReply },
352
350
  );
353
351
  const gaps = parsed.angles.slice(0, MAX_ANGLES - 1);
354
- await report(
352
+ await stepReport(
355
353
  gaps.length === 0
356
354
  ? "The brief is covered; writing it up."
357
355
  : `Following up ${gaps.length} ${plural(gaps.length, "gap")}.`,
@@ -371,12 +369,12 @@ export async function writeReport(
371
369
  brief: Brief,
372
370
  notes: readonly Note[],
373
371
  ): Promise<{ report: string; summary: string }> {
374
- await report(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
375
- const written = await stepGenerateClassified(
372
+ await stepReport(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
373
+ const written = await stepGenerateOrFail(
376
374
  `${briefText(brief)}\n\nFindings:\n${notes.map(noteText).join("\n\n")}`,
377
375
  { system: REPORT_SYSTEM },
378
376
  );
379
- const summary = await stepGenerateClassified(`Topic: ${topic}\n\nReport:\n${written}`, {
377
+ const summary = await stepGenerateOrFail(`Topic: ${topic}\n\nReport:\n${written}`, {
380
378
  system: BRIEF_SUMMARY_SYSTEM,
381
379
  });
382
380
  return { report: written, summary };
@@ -390,7 +388,7 @@ export async function writeReport(
390
388
  * parameters carry `_` for the same reason.
391
389
  */
392
390
  export async function file(_requestedBy: string, _topic: string): Promise<string> {
393
- await report("Filing the findings.");
391
+ await stepReport("Filing the findings.");
394
392
  return "filed";
395
393
  }
396
394
 
@@ -406,7 +404,7 @@ async function nextAction(
406
404
  seen: readonly string[],
407
405
  left: number,
408
406
  ): Promise<Action> {
409
- return await stepGenerateJsonClassified(
407
+ return await stepGenerateJsonOrFail(
410
408
  `${briefText(brief)}\n\nYour angle: ${angle}\n` +
411
409
  `Actions left: ${left}\n\n` +
412
410
  (seen.length === 0 ? "You have not looked at anything yet." : seen.join("\n\n")),
@@ -423,7 +421,7 @@ async function nextAction(
423
421
  * pages exist" and gets run again, differently worded, until the budget is gone.
424
422
  */
425
423
  async function search(query: string): Promise<{ summary: string; sources: Source[] }> {
426
- await report(`Searching: ${query}`);
424
+ await stepReport(`Searching: ${query}`);
427
425
  try {
428
426
  const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
429
427
  query,
@@ -445,7 +443,7 @@ async function search(query: string): Promise<{ summary: string; sources: Source
445
443
  };
446
444
  } catch (err: unknown) {
447
445
  const summary = `That search failed: ${errorMessage(err)}`;
448
- await report(summary);
446
+ await stepReport(summary);
449
447
  return { summary, sources: [] };
450
448
  }
451
449
  }
@@ -475,7 +473,7 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
475
473
  if (seen.length === 0) {
476
474
  return { angle, findings: "Nothing was found on this angle.", sources: [] };
477
475
  }
478
- const parsed = await stepGenerateJsonClassified(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
476
+ const parsed = await stepGenerateJsonOrFail(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
479
477
  system: COMPRESS_SYSTEM,
480
478
  schema: CompressReply,
481
479
  });
@@ -492,11 +490,11 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
492
490
  //
493
491
  // There is none left, and its absence is the point. This desk carried an `ask()`
494
492
  // and an `askJson()` whose whole body was `.catch(throwStepError)`; the SDK's
495
- // `stepGenerateClassified` and `stepGenerateJsonClassified`
493
+ // `stepGenerateOrFail` and `stepGenerateJsonOrFail`
496
494
  // (`@alexkroman1/aai/step-errors`) ARE that call — the `/step` one with the
497
495
  // gateway's verdict classified, so a terminal failure stays terminal and a rate
498
496
  // limit becomes a `RetryableError` carrying the delay the gateway itself named.
499
- // `stepGenerateJsonClassified` also owns the four things every JSON stage used
497
+ // `stepGenerateJsonOrFail` also owns the four things every JSON stage used
500
498
  // to re-derive — unwrap the fence, parse, reject a non-object, check the shape —
501
499
  // and throws PLAINLY when any of them misses, which is what makes a malformed
502
500
  // reply a retry rather than a failure.
@@ -1,6 +1,6 @@
1
1
  import type { ToolContext } from "@alexkroman1/aai";
2
2
  import { isToolFailure } from "@alexkroman1/aai";
3
- import { createToolContext, ok } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test } from "vitest";
5
5
  import type { AuthResult } from "./authenticate.ts";
6
6
  import type { StagedResult } from "./pending.ts";
@@ -32,7 +32,7 @@ function makeCtx(): ToolContext {
32
32
  /** A context already authenticated as `userId`, via the real tool. */
33
33
  async function authedCtx(email: string): Promise<ToolContext> {
34
34
  const ctx = makeCtx();
35
- ok<AuthResult>(await findUserIdByEmail.execute({ email }, ctx));
35
+ expectToolOk<AuthResult>(await findUserIdByEmail.execute({ email }, ctx));
36
36
  return ctx;
37
37
  }
38
38
 
@@ -47,19 +47,19 @@ async function authedCtx(email: string): Promise<ToolContext> {
47
47
  * would let this gate pass while carrying nothing to confirm.
48
48
  */
49
49
  async function confirmed<R = unknown>(staged: unknown, ctx: ToolContext): Promise<R> {
50
- const stage = ok<StagedResult>(staged);
50
+ const stage = expectToolOk<StagedResult>(staged);
51
51
  // A throw rather than an `expect`, which biome's `noMisplacedAssertion`
52
52
  // rightly refuses outside a test body. A staged change with no sentence to
53
53
  // read back is the one shape that would let the gate pass while carrying
54
54
  // nothing to confirm, so it stops the spec here rather than downstream.
55
55
  if (stage.read_back.length === 0) throw new Error(`${stage.staged} staged with no readback`);
56
- return ok<R>(await confirmChange.execute({}, ctx));
56
+ return expectToolOk<R>(await confirmChange.execute({}, ctx));
57
57
  }
58
58
 
59
59
  describe("authentication", () => {
60
60
  test("find_user_id_by_email is case-insensitive and authenticates the session", async () => {
61
61
  const ctx = makeCtx();
62
- const result = ok<AuthResult>(
62
+ const result = expectToolOk<AuthResult>(
63
63
  await findUserIdByEmail.execute({ email: "OLIVIA.ITO5204@EXAMPLE.COM" }, ctx),
64
64
  );
65
65
  expect(result.user_id).toBe("olivia_ito_3591");
@@ -81,7 +81,7 @@ describe("authentication", () => {
81
81
 
82
82
  test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
83
83
  const ctx = makeCtx();
84
- const found = ok<AuthResult>(
84
+ const found = expectToolOk<AuthResult>(
85
85
  await findUserIdByNameZip.execute(
86
86
  { first_name: "aarav", last_name: "ANDERSON", zip: "19031" },
87
87
  ctx,
@@ -97,13 +97,13 @@ describe("authentication", () => {
97
97
  });
98
98
 
99
99
  test("two customers share a first name — the zip is what separates them", async () => {
100
- const a = ok<AuthResult>(
100
+ const a = expectToolOk<AuthResult>(
101
101
  await findUserIdByNameZip.execute(
102
102
  { first_name: "Aarav", last_name: "Anderson", zip: "19031" },
103
103
  makeCtx(),
104
104
  ),
105
105
  );
106
- const b = ok<AuthResult>(
106
+ const b = expectToolOk<AuthResult>(
107
107
  await findUserIdByNameZip.execute(
108
108
  { first_name: "Aarav", last_name: "Gonzalez", zip: "78268" },
109
109
  makeCtx(),
@@ -172,7 +172,7 @@ interface ProductTypesResult {
172
172
  describe("read tools", () => {
173
173
  test("get_user_details returns the caller's profile with gift-card balances", async () => {
174
174
  const ctx = await authedCtx("olivia.ito5204@example.com");
175
- const result = ok<UserDetailsResult>(
175
+ const result = expectToolOk<UserDetailsResult>(
176
176
  await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx),
177
177
  );
178
178
  expect(result.email).toBe("olivia.ito5204@example.com");
@@ -189,7 +189,7 @@ describe("read tools", () => {
189
189
 
190
190
  test("get_order_details resolves shorthand and sets focus", async () => {
191
191
  const ctx = await authedCtx("olivia.ito5204@example.com");
192
- const result = ok<OrderDetailsResult>(
192
+ const result = expectToolOk<OrderDetailsResult>(
193
193
  await getOrderDetails.execute({ order_id: "the delivered one" }, ctx),
194
194
  );
195
195
  expect(result.order_id).toBe("#W5866402");
@@ -203,7 +203,7 @@ describe("read tools", () => {
203
203
  });
204
204
 
205
205
  test("get_product_details lists variants and needs no authentication", async () => {
206
- const result = ok<ProductDetailsResult>(
206
+ const result = expectToolOk<ProductDetailsResult>(
207
207
  await getProductDetails.execute({ product_id: "9832717871" }, makeCtx()),
208
208
  );
209
209
  expect(result.name).toBe("Tea Kettle");
@@ -217,7 +217,7 @@ describe("read tools", () => {
217
217
  });
218
218
 
219
219
  test("get_item_details resolves an item without knowing its product", async () => {
220
- const result = ok<ItemDetailsResult>(
220
+ const result = expectToolOk<ItemDetailsResult>(
221
221
  await getItemDetails.execute({ item_id: "3909406921" }, makeCtx()),
222
222
  );
223
223
  expect(result.price).toBe(98.25);
@@ -225,7 +225,9 @@ describe("read tools", () => {
225
225
  });
226
226
 
227
227
  test("list_all_product_types returns all 50, sorted by name", async () => {
228
- const result = ok<ProductTypesResult>(await listAllProductTypes.execute({}, makeCtx()));
228
+ const result = expectToolOk<ProductTypesResult>(
229
+ await listAllProductTypes.execute({}, makeCtx()),
230
+ );
229
231
  expect(Object.keys(result.products)).toHaveLength(50);
230
232
  const names = Object.keys(result.products);
231
233
  // localeCompare, not the default lexicographic sort — the tool sorts for a
@@ -1148,7 +1150,7 @@ interface TransferResult {
1148
1150
 
1149
1151
  describe("transfer_to_human_agents", () => {
1150
1152
  test("works without authentication — the escape hatch cannot be gated", async () => {
1151
- const result = ok<TransferResult>(
1153
+ const result = expectToolOk<TransferResult>(
1152
1154
  await transferToHumanAgents.execute(
1153
1155
  { summary: "Caller wants to dispute a charge from 2019." },
1154
1156
  makeCtx(),
@@ -1160,7 +1162,7 @@ describe("transfer_to_human_agents", () => {
1160
1162
 
1161
1163
  test("the handoff is terminal, so the call cannot be worked afterwards", async () => {
1162
1164
  const ctx = await authedCtx("olivia.ito5204@example.com");
1163
- ok(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
1165
+ expectToolOk(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
1164
1166
 
1165
1167
  const at = callFlow.position(ctx);
1166
1168
  expect(at.state).toBe("transferred");