@alexkroman1/aai-cli 6.11.0 → 7.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 (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +40 -19
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +1 -1
  28. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  29. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  30. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  31. package/dist/templates/call-audit/agent.test.ts +57 -204
  32. package/dist/templates/call-audit/agent.ts +32 -19
  33. package/dist/templates/call-audit/client.tsx +17 -54
  34. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  35. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  36. package/dist/templates/call-audit/workflows/media.ts +2 -12
  37. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  38. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  39. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  40. package/dist/templates/dispatch-center/client.tsx +239 -129
  41. package/dist/templates/dispatch-center/shared.ts +99 -1
  42. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  43. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  44. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  45. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  46. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  47. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  48. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  49. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  50. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  51. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  52. package/dist/templates/health-assistant/agent.test.ts +22 -4
  53. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  54. package/dist/templates/infocom-adventure/client.tsx +246 -164
  55. package/dist/templates/link-digest/agent.test.ts +24 -19
  56. package/dist/templates/link-digest/client.tsx +47 -61
  57. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  58. package/dist/templates/night-owl/agent.test.ts +70 -19
  59. package/dist/templates/night-owl/agent.ts +5 -0
  60. package/dist/templates/night-owl/client.tsx +56 -56
  61. package/dist/templates/night-owl/shared.ts +24 -0
  62. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  63. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  64. package/dist/templates/pizza-ordering/client.tsx +9 -26
  65. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  66. package/dist/templates/plan-and-execute/agent.ts +1 -1
  67. package/dist/templates/plan-and-execute/client.tsx +12 -15
  68. package/dist/templates/plan-and-execute/shared.ts +71 -2
  69. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  70. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  71. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  72. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  73. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  74. package/dist/templates/podcast-digest/agent.ts +139 -0
  75. package/dist/templates/podcast-digest/client.tsx +154 -0
  76. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  77. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  78. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  79. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  80. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  81. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  82. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  83. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  84. package/dist/templates/redline/agent.test.ts +17 -15
  85. package/dist/templates/redline/client.tsx +12 -12
  86. package/dist/templates/redline/workflows/redline.ts +19 -31
  87. package/dist/templates/research-workflow/agent.test.ts +60 -59
  88. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  89. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  90. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  91. package/dist/templates/retail/agent.test.ts +26 -23
  92. package/dist/templates/retail/client.tsx +226 -117
  93. package/dist/templates/retail/registry.test.ts +38 -6
  94. package/dist/templates/retail/store.test.ts +82 -15
  95. package/dist/templates/retail/store.ts +174 -47
  96. package/dist/templates/retail/system-prompt.md +11 -2
  97. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  98. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  99. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  100. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  101. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  102. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  105. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  106. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  107. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  108. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  109. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  110. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  111. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  112. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  113. package/dist/templates/solo-rpg/client.tsx +38 -37
  114. package/dist/templates/solo-rpg/shared.ts +145 -19
  115. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  116. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  117. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  118. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  119. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  120. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  121. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  122. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  123. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  124. package/dist/templates/spoken-summary/agent.ts +30 -17
  125. package/dist/templates/spoken-summary/client.tsx +10 -50
  126. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  127. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  128. package/dist/templates/support-line/agent.test.ts +11 -16
  129. package/dist/templates/support-line/agent.ts +1 -1
  130. package/dist/templates/support-line/client.tsx +9 -9
  131. package/dist/templates/support-line/nodes.ts +100 -0
  132. package/dist/templates/support-line/procedure.ts +407 -0
  133. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  134. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  135. package/dist/templates/transcription-workflow/agent.ts +1 -1
  136. package/dist/templates/transcription-workflow/client.tsx +17 -42
  137. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  138. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  139. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  140. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  141. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  142. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  143. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  144. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  145. package/dist/templates/travel-concierge/client.tsx +11 -23
  146. package/dist/templates/travel-concierge/routing.ts +34 -15
  147. package/dist/templates/travel-concierge/shared.ts +70 -3
  148. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  149. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  153. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  154. package/dist/worker-bundler.mjs +1 -1
  155. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  156. package/dist/workflow-bundler.d.ts +6 -1
  157. package/dist/workflow.d.ts +1 -1
  158. package/package.json +5 -4
  159. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -1,9 +1,6 @@
1
- import {
2
- isTerminal,
3
- tool,
4
- type WorkflowOutputOf,
5
- type WorkflowRunSnapshot,
6
- } from "@alexkroman1/aai";
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { plural } from "@alexkroman1/aai/utils";
3
+ import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
7
4
  import { research } from "../shared.ts";
8
5
 
9
6
  /** How many past runs the status tool will look at. Newest first. */
@@ -12,17 +9,18 @@ const RECENT_RUNS = 3;
12
9
  /**
13
10
  * One line a voice agent can read aloud about a run.
14
11
  *
15
- * `WorkflowOutputOf` is what names the output type — the same helper a page uses
16
- * to type `run.output`, and the reason this signature does not have to reach
17
- * past the declaration into the body's own return type.
12
+ * `WorkflowRunOf` is the snapshot with its output already typed — the
13
+ * `WorkflowRunSnapshot<WorkflowOutputOf<typeof research>>` this file used to
14
+ * compose by hand, which cost a three-name import for one type. Still the
15
+ * discriminated union, so `isTerminal` below narrows exactly as it did.
18
16
  */
19
- function describeRun(run: WorkflowRunSnapshot<WorkflowOutputOf<typeof research>>): string {
17
+ function describeRun(run: WorkflowRunOf<typeof research>): string {
20
18
  // `isTerminal` narrows to the three finished statuses, which is what makes
21
19
  // `run.output` and `run.error` reachable without a cast.
22
20
  if (!isTerminal(run)) return "Still working on it.";
23
21
  switch (run.status) {
24
22
  case "completed":
25
- return `Done: ${run.output.summary} (${run.output.sources} sources)`;
23
+ return `Done: ${run.output.summary} (${run.output.sources} ${plural(run.output.sources, "source")})`;
26
24
  case "failed":
27
25
  return `That one failed: ${run.error}`;
28
26
  default:
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * ## Every stage REPORTS, and the report goes two places
37
37
  *
38
- * `report()` (`@alexkroman1/aai/utils`) writes to the run's own stream — which
38
+ * `report()` (`@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,16 +49,10 @@
49
49
  * researcher CONCLUDED, which is exactly what the step returns.
50
50
  */
51
51
 
52
- import { throwStepError } from "@alexkroman1/aai/step-errors";
52
+ import { mapConcurrent, report } from "@alexkroman1/aai/step";
53
+ import { stepGenerateClassified, stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
53
54
  import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
54
- import {
55
- errorMessage,
56
- isToolFailure,
57
- mapInBatches,
58
- report,
59
- stepGenerate,
60
- stepGenerateJson,
61
- } from "@alexkroman1/aai/utils";
55
+ import { errorMessage, isToolFailure, plural } from "@alexkroman1/aai/utils";
62
56
  import { sleep } from "workflow";
63
57
  import { z } from "zod";
64
58
  import {
@@ -108,9 +102,9 @@ export type Source = { title: string; url: string };
108
102
 
109
103
  // ---- What each stage's model call has to come back as ------------------------
110
104
  //
111
- // `stepGenerateJson` validates against these, so a reply that missed is a plain
112
- // throw and therefore a retry — where the hand-rolled `askJson<T>()` this
113
- // replaces returned a value the compiler believed and nothing checked. They are
105
+ // `stepGenerateJsonClassified` validates against these, so a reply that missed
106
+ // is a plain throw and therefore a retry — where the hand-rolled `askJson<T>()`
107
+ // this replaces returned a value the compiler believed and nothing checked. They are
114
108
  // deliberately LENIENT wherever the old hand-written coercion was: a model that
115
109
  // put one number in an array of strings should cost that element, not the whole
116
110
  // pass.
@@ -221,12 +215,14 @@ export async function researchFlow(input: { topic: string; requestedBy: string }
221
215
  // failed angle fails the RUN: its finished siblings are already journaled, so
222
216
  // the resume replays them for free and re-issues only what is missing, where
223
217
  // catching here would file a report with a silent hole in it.
224
- const first = await mapInBatches(angles, ANGLE_CONCURRENCY, (angle) => investigate(brief, angle));
218
+ const first = await mapConcurrent(angles, ANGLE_CONCURRENCY, (angle) =>
219
+ investigate(brief, angle),
220
+ );
225
221
 
226
222
  // The supervisor's second look. Usually empty — a second wave costs the caller
227
223
  // minutes, and the prompt says so.
228
224
  const gaps = await findGaps(brief, first);
229
- const second = await mapInBatches(gaps, ANGLE_CONCURRENCY, (angle) => investigate(brief, angle));
225
+ const second = await mapConcurrent(gaps, ANGLE_CONCURRENCY, (angle) => investigate(brief, angle));
230
226
 
231
227
  const notes = [...first, ...second];
232
228
  const written = await writeReport(input.topic, brief, notes);
@@ -261,10 +257,9 @@ export async function writeBrief(topic: string): Promise<Brief> {
261
257
  "use step";
262
258
 
263
259
  await report(`Working out what "${topic}" is really asking.`);
264
- const parsed = await askJson(
260
+ const parsed = await stepGenerateJsonClassified(
265
261
  `Research request, as the caller said it: ${topic}`,
266
- BRIEF_SYSTEM,
267
- BriefReply,
262
+ { system: BRIEF_SYSTEM, schema: BriefReply },
268
263
  );
269
264
  return { brief: parsed.brief || topic, criteria: parsed.criteria.slice(0, MAX_ANGLES) };
270
265
  }
@@ -279,7 +274,10 @@ export async function writeBrief(topic: string): Promise<Brief> {
279
274
  export async function planAngles(brief: Brief): Promise<string[]> {
280
275
  "use step";
281
276
 
282
- const parsed = await askJson(briefText(brief), PLAN_SYSTEM, AnglesReply);
277
+ const parsed = await stepGenerateJsonClassified(briefText(brief), {
278
+ system: PLAN_SYSTEM,
279
+ schema: AnglesReply,
280
+ });
283
281
  const angles = parsed.angles.slice(0, MAX_ANGLES);
284
282
  if (angles.length === 0) {
285
283
  // Nothing to fan out over is a plan failure, not an empty result: the brief
@@ -287,7 +285,7 @@ export async function planAngles(brief: Brief): Promise<string[]> {
287
285
  await report("No angles came back; researching the brief itself.");
288
286
  return [brief.brief];
289
287
  }
290
- await report(`Researching ${angles.length} angle${angles.length === 1 ? "" : "s"}.`);
288
+ await report(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
291
289
  return angles;
292
290
  }
293
291
 
@@ -341,16 +339,15 @@ export async function findGaps(brief: Brief, notes: readonly Note[]): Promise<st
341
339
  "use step";
342
340
 
343
341
  if (notes.length === 0) return [];
344
- const parsed = await askJson(
342
+ const parsed = await stepGenerateJsonClassified(
345
343
  `${briefText(brief)}\n\nWhat came back:\n${notes.map(noteText).join("\n\n")}`,
346
- GAPS_SYSTEM,
347
- AnglesReply,
344
+ { system: GAPS_SYSTEM, schema: AnglesReply },
348
345
  );
349
346
  const gaps = parsed.angles.slice(0, MAX_ANGLES - 1);
350
347
  await report(
351
348
  gaps.length === 0
352
349
  ? "The brief is covered; writing it up."
353
- : `Following up ${gaps.length} gap${gaps.length === 1 ? "" : "s"}.`,
350
+ : `Following up ${gaps.length} ${plural(gaps.length, "gap")}.`,
354
351
  );
355
352
  return gaps;
356
353
  }
@@ -369,12 +366,14 @@ export async function writeReport(
369
366
  ): Promise<{ report: string; summary: string }> {
370
367
  "use step";
371
368
 
372
- await report(`Writing up ${notes.length} angle${notes.length === 1 ? "" : "s"}.`);
373
- const written = await ask(
369
+ await report(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
370
+ const written = await stepGenerateClassified(
374
371
  `${briefText(brief)}\n\nFindings:\n${notes.map(noteText).join("\n\n")}`,
375
- REPORT_SYSTEM,
372
+ { system: REPORT_SYSTEM },
376
373
  );
377
- const summary = await ask(`Topic: ${topic}\n\nReport:\n${written}`, BRIEF_SUMMARY_SYSTEM);
374
+ const summary = await stepGenerateClassified(`Topic: ${topic}\n\nReport:\n${written}`, {
375
+ system: BRIEF_SUMMARY_SYSTEM,
376
+ });
378
377
  return { report: written, summary };
379
378
  }
380
379
 
@@ -404,12 +403,11 @@ async function nextAction(
404
403
  seen: readonly string[],
405
404
  left: number,
406
405
  ): Promise<Action> {
407
- return await askJson(
406
+ return await stepGenerateJsonClassified(
408
407
  `${briefText(brief)}\n\nYour angle: ${angle}\n` +
409
408
  `Actions left: ${left}\n\n` +
410
409
  (seen.length === 0 ? "You have not looked at anything yet." : seen.join("\n\n")),
411
- RESEARCH_SYSTEM,
412
- ActionReply,
410
+ { system: RESEARCH_SYSTEM, schema: ActionReply },
413
411
  );
414
412
  }
415
413
 
@@ -474,11 +472,10 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
474
472
  if (seen.length === 0) {
475
473
  return { angle, findings: "Nothing was found on this angle.", sources: [] };
476
474
  }
477
- const parsed = await askJson(
478
- `Angle: ${angle}\n\n${seen.join("\n\n")}`,
479
- COMPRESS_SYSTEM,
480
- CompressReply,
481
- );
475
+ const parsed = await stepGenerateJsonClassified(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
476
+ system: COMPRESS_SYSTEM,
477
+ schema: CompressReply,
478
+ });
482
479
  return {
483
480
  angle,
484
481
  findings: parsed.findings ?? seen.join("\n\n"),
@@ -489,31 +486,17 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
489
486
  }
490
487
 
491
488
  // ---- Model plumbing ---------------------------------------------------------
492
-
493
- /**
494
- * `stepGenerate`, with this desk's retry POLICY on top.
495
- *
496
- * The SDK classifies the gateway's failure (`StepGenerateError.retryable`) and
497
- * stops there, deliberately: whether a terminal failure should burn the step's
498
- * remaining attempts is the caller's call. `throwStepError` is that call made
499
- * one way terminal stays terminal, and a rate limit becomes a `RetryableError`
500
- * carrying the delay the gateway itself named.
501
- */
502
- async function ask(prompt: string, system: string): Promise<string> {
503
- return await stepGenerate(prompt, { system }).catch(throwStepError);
504
- }
505
-
506
- /**
507
- * The same call, for a stage whose reply is JSON of a known shape.
508
- *
509
- * `stepGenerateJson` owns the four things every such stage used to re-derive —
510
- * unwrap the fence, parse, reject a non-object, check the shape — and throws
511
- * PLAINLY when any of them misses, which is what makes a malformed reply a
512
- * retry rather than a failure.
513
- */
514
- async function askJson<S extends z.ZodType>(prompt: string, system: string, schema: S) {
515
- return await stepGenerateJson(prompt, { system, schema }).catch(throwStepError);
516
- }
489
+ //
490
+ // There is none left, and its absence is the point. This desk carried an `ask()`
491
+ // and an `askJson()` whose whole body was `.catch(throwStepError)`; the SDK's
492
+ // `stepGenerateClassified` and `stepGenerateJsonClassified`
493
+ // (`@alexkroman1/aai/step-errors`) ARE that call the `/step` one with the
494
+ // gateway's verdict classified, so a terminal failure stays terminal and a rate
495
+ // limit becomes a `RetryableError` carrying the delay the gateway itself named.
496
+ // `stepGenerateJsonClassified` also owns the four things every JSON stage used
497
+ // to re-derive — unwrap the fence, parse, reject a non-object, check the shape
498
+ // and throws PLAINLY when any of them misses, which is what makes a malformed
499
+ // reply a retry rather than a failure.
517
500
 
518
501
  // ---- Pure helpers -----------------------------------------------------------
519
502
 
@@ -1,10 +1,10 @@
1
1
  import type { ToolContext } from "@alexkroman1/aai";
2
2
  import { isToolFailure } from "@alexkroman1/aai";
3
- import { createToolContext } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, ok } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test } from "vitest";
5
5
  import type { AuthResult } from "./authenticate.ts";
6
6
  import type { Address } from "./shared.ts";
7
- import { retailSlot } from "./store.ts";
7
+ import { callFlow, retailSlot } from "./store.ts";
8
8
  import cancelPendingOrder from "./tools/cancel_pending_order.ts";
9
9
  import exchangeDeliveredOrderItems from "./tools/exchange_delivered_order_items.ts";
10
10
  import findUserIdByEmail from "./tools/find_user_id_by_email.ts";
@@ -27,27 +27,6 @@ function makeCtx(): ToolContext {
27
27
  return createToolContext();
28
28
  }
29
29
 
30
- /**
31
- * The success half of a tool result, or a thrown failure carrying the tool's
32
- * own message.
33
- *
34
- * `ToolDef["execute"]`'s public signature always returns `unknown` — the wire
35
- * type is fixed so any tool is assignable to `ToolDef`, whatever its body
36
- * really returns — so every test that reads a field back off a result needed a
37
- * cast. There were EIGHTEEN of them here, each the same three lines, which is
38
- * the shape the repo's guides call a missing typed seam: a concentration of
39
- * identical casts is one narrowing that belongs in one helper, not a cast per
40
- * assertion. This is that helper, and it is the file's only cast.
41
- *
42
- * It also removes the second half of the boilerplate: a `ToolFailure` fails the
43
- * test HERE, naming what the tool refused, rather than surfacing three lines
44
- * later as `undefined` on a field nobody assigned.
45
- */
46
- function ok<T>(result: unknown): T {
47
- if (isToolFailure(result)) throw new Error(`tool refused: ${result.error}`);
48
- return result as T;
49
- }
50
-
51
30
  /** A context already authenticated as `userId`, via the real tool. */
52
31
  async function authedCtx(email: string): Promise<ToolContext> {
53
32
  const ctx = makeCtx();
@@ -63,6 +42,8 @@ describe("authentication", () => {
63
42
  );
64
43
  expect(result.user_id).toBe("olivia_ito_3591");
65
44
  expect(retailSlot.get(ctx).authenticatedUserId).toBe("olivia_ito_3591");
45
+ // Two facts, two homes: the store latches WHO, the flow holds WHETHER.
46
+ expect(callFlow.position(ctx).state).toBe("serving");
66
47
  });
67
48
 
68
49
  test("an unknown email is refused and leaves the session unauthenticated", async () => {
@@ -70,6 +51,9 @@ describe("authentication", () => {
70
51
  const result = await findUserIdByEmail.execute({ email: "nobody@example.com" }, ctx);
71
52
  expect(isToolFailure(result)).toBe(true);
72
53
  expect(retailSlot.get(ctx).authenticatedUserId).toBeNull();
54
+ // `IDENTIFIED` is not sent when the body answers a `ToolFailure`, so a
55
+ // lookup that found nobody cannot leave the call one step ahead of itself.
56
+ expect(callFlow.position(ctx).state).toBe("identifying");
73
57
  });
74
58
 
75
59
  test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
@@ -121,6 +105,9 @@ describe("authentication", () => {
121
105
  );
122
106
  expect(isToolFailure(switched) && switched.error.toLowerCase()).toContain("one customer");
123
107
  expect(retailSlot.get(ctx).authenticatedUserId).toBe("olivia_ito_3591");
108
+ // The tool is legal in `serving` — the refusal comes from `authenticateAs`,
109
+ // not from the gate — so the call stays exactly where it was.
110
+ expect(callFlow.position(ctx).state).toBe("serving");
124
111
  });
125
112
 
126
113
  test("switching via name + zip is refused too — both doors, one lock", async () => {
@@ -1090,4 +1077,20 @@ describe("transfer_to_human_agents", () => {
1090
1077
  expect(isToolFailure(result)).toBe(false);
1091
1078
  expect(result.transferred).toBe(true);
1092
1079
  });
1080
+
1081
+ test("the handoff is terminal, so the call cannot be worked afterwards", async () => {
1082
+ const ctx = await authedCtx("olivia.ito5204@example.com");
1083
+ ok(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
1084
+
1085
+ const at = callFlow.position(ctx);
1086
+ expect(at.state).toBe("transferred");
1087
+ expect(at.done).toBe(true);
1088
+
1089
+ // The policy's "say nothing else after that" used to be enforced by
1090
+ // nothing: every tool stayed callable, so a model that kept going kept
1091
+ // acting on a call it had given away.
1092
+ const refused = await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx);
1093
+ expect(isToolFailure(refused)).toBe(true);
1094
+ expect(isToolFailure(refused) && refused.error).toContain('"transferred"');
1095
+ });
1093
1096
  });