@alexkroman1/aai-cli 6.10.1 → 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 +93 -14
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +13 -4
  28. package/dist/scaffold/vite.config.ts +1 -1
  29. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  30. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  31. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  32. package/dist/templates/call-audit/agent.test.ts +818 -0
  33. package/dist/templates/call-audit/agent.ts +171 -0
  34. package/dist/templates/call-audit/client.tsx +198 -0
  35. package/dist/templates/call-audit/workflows/audit.ts +298 -0
  36. package/dist/templates/call-audit/workflows/ingest.ts +251 -0
  37. package/dist/templates/call-audit/workflows/media.ts +637 -0
  38. package/dist/templates/call-audit/workflows/summarize.ts +201 -0
  39. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  40. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  41. package/dist/templates/dispatch-center/client.tsx +239 -129
  42. package/dist/templates/dispatch-center/shared.ts +99 -1
  43. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  44. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  45. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  46. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  47. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  48. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  49. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  50. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  51. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  52. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  53. package/dist/templates/health-assistant/agent.test.ts +22 -4
  54. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  55. package/dist/templates/infocom-adventure/client.tsx +246 -164
  56. package/dist/templates/link-digest/agent.test.ts +24 -19
  57. package/dist/templates/link-digest/client.tsx +47 -61
  58. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  59. package/dist/templates/night-owl/agent.test.ts +70 -19
  60. package/dist/templates/night-owl/agent.ts +5 -0
  61. package/dist/templates/night-owl/client.tsx +56 -56
  62. package/dist/templates/night-owl/shared.ts +24 -0
  63. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  64. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  65. package/dist/templates/pizza-ordering/client.tsx +9 -26
  66. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  67. package/dist/templates/plan-and-execute/agent.ts +1 -1
  68. package/dist/templates/plan-and-execute/client.tsx +12 -15
  69. package/dist/templates/plan-and-execute/shared.ts +71 -2
  70. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  71. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  72. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  73. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  74. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  75. package/dist/templates/podcast-digest/agent.ts +139 -0
  76. package/dist/templates/podcast-digest/client.tsx +154 -0
  77. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  78. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  79. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  80. package/dist/templates/recap-workflow/agent.test.ts +107 -96
  81. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  82. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  83. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  84. package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
  85. package/dist/templates/redline/agent.test.ts +17 -15
  86. package/dist/templates/redline/client.tsx +12 -12
  87. package/dist/templates/redline/workflows/redline.ts +19 -31
  88. package/dist/templates/research-workflow/agent.test.ts +60 -59
  89. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  90. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  91. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  92. package/dist/templates/retail/agent.test.ts +26 -23
  93. package/dist/templates/retail/client.tsx +226 -117
  94. package/dist/templates/retail/registry.test.ts +38 -6
  95. package/dist/templates/retail/store.test.ts +82 -15
  96. package/dist/templates/retail/store.ts +174 -47
  97. package/dist/templates/retail/system-prompt.md +11 -2
  98. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  99. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  100. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  101. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  102. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  105. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  106. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  107. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  108. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  109. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  110. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  111. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  112. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  113. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  114. package/dist/templates/solo-rpg/client.tsx +38 -37
  115. package/dist/templates/solo-rpg/shared.ts +145 -19
  116. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  117. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  118. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  119. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  120. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  121. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  122. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  123. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  124. package/dist/templates/spoken-summary/agent.test.ts +314 -0
  125. package/dist/templates/spoken-summary/agent.ts +155 -0
  126. package/dist/templates/spoken-summary/client.tsx +185 -0
  127. package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
  128. package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
  129. package/dist/templates/support-line/agent.test.ts +11 -16
  130. package/dist/templates/support-line/agent.ts +1 -1
  131. package/dist/templates/support-line/client.tsx +9 -9
  132. package/dist/templates/support-line/nodes.ts +100 -0
  133. package/dist/templates/support-line/procedure.ts +407 -0
  134. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  135. package/dist/templates/transcription-workflow/agent.test.ts +246 -123
  136. package/dist/templates/transcription-workflow/agent.ts +21 -7
  137. package/dist/templates/transcription-workflow/client.tsx +17 -42
  138. package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
  139. package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
  140. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  141. package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
  142. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  143. package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
  144. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
  145. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  146. package/dist/templates/travel-concierge/client.tsx +11 -23
  147. package/dist/templates/travel-concierge/routing.ts +34 -15
  148. package/dist/templates/travel-concierge/shared.ts +70 -3
  149. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  153. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  154. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  155. package/dist/worker-bundler.mjs +1 -1
  156. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  157. package/dist/workflow-bundler.d.ts +6 -1
  158. package/dist/workflow.d.ts +1 -1
  159. package/package.json +5 -4
  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,5 @@
1
- import {
2
- isTerminal,
3
- tool,
4
- type WorkflowOutputOf,
5
- type WorkflowRunSnapshot,
6
- } from "@alexkroman1/aai";
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
7
3
  import { recap } from "../shared.ts";
8
4
 
9
5
  /** How many past runs the status tool will look at. Newest first. */
@@ -14,10 +10,13 @@ const RECENT_RUNS = 3;
14
10
  *
15
11
  * This is the QUERY, and `isTerminal` is what makes it typed: it narrows to the
16
12
  * three finished statuses, which is what puts `run.output` and `run.error`
17
- * within reach without a cast. `WorkflowOutputOf` names the output type from the
18
- * declaration, so this signature never reaches past it into the body.
13
+ * within reach without a cast. `WorkflowRunOf` is the snapshot with that output
14
+ * already named from the declaration the
15
+ * `WorkflowRunSnapshot<WorkflowOutputOf<typeof recap>>` this file used to
16
+ * compose by hand, for a three-name import — so the signature never reaches past
17
+ * the declaration into the body, and it is still the discriminated union.
19
18
  */
20
- function describeRun(run: WorkflowRunSnapshot<WorkflowOutputOf<typeof recap>>): string {
19
+ function describeRun(run: WorkflowRunOf<typeof recap>): string {
21
20
  if (!isTerminal(run)) return "Still working on that one.";
22
21
  switch (run.status) {
23
22
  case "completed": {
@@ -1,4 +1,5 @@
1
- import { isTerminal, tool } from "@alexkroman1/aai";
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { isTerminal } from "@alexkroman1/aai/workflow-api";
2
3
  import { z } from "zod";
3
4
  import { recap, SAMPLE_RECORDING } from "../shared.ts";
4
5
 
@@ -55,7 +55,7 @@
55
55
  * Everything the desk claims to do. `submitRecording`, `checkTranscript` and
56
56
  * `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
57
57
  * `DELETE` on `/v2/transcript`), and `summarize` is a real model call through
58
- * `stepGenerate`. The BATCH API is what makes the polling port honest: it
58
+ * `stepGenerateJsonClassified`. The BATCH API is what makes the polling port honest: it
59
59
  * answers with a job id in milliseconds and finishes minutes later, so the wait
60
60
  * is the provider's, not a `setTimeout` this template chose. (Its sibling
61
61
  * `transcription-workflow` takes the other endpoint — the sync one, which answers in
@@ -67,16 +67,14 @@
67
67
  * just your shell.
68
68
  */
69
69
 
70
- import { throwStepError, toStepError } from "@alexkroman1/aai/step-errors";
70
+ import { report, requireStepEnv, stepFetch } from "@alexkroman1/aai/step";
71
71
  import {
72
- errorMessage,
73
- isRecord,
74
- omitUndefined,
75
- report,
76
- requireStepEnv,
77
- stepFetch,
78
- stepGenerateJson,
79
- } from "@alexkroman1/aai/utils";
72
+ stepFetchOk,
73
+ stepGenerateJsonClassified,
74
+ stepTranscribeSubmitClassified,
75
+ toStepError,
76
+ } from "@alexkroman1/aai/step-errors";
77
+ import { errorMessage, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
80
78
  import { createHook, FatalError, sleep } from "workflow";
81
79
  import { z } from "zod";
82
80
  import { retentionToken } from "./tokens.ts";
@@ -150,7 +148,7 @@ const POINTS = 3;
150
148
  /**
151
149
  * The shape the model must answer in.
152
150
  *
153
- * `stepGenerateJson` validates against this and throws PLAINLY when the reply
151
+ * `stepGenerateJsonClassified` validates against this and throws PLAINLY when the reply
154
152
  * misses, which is the retry policy in one distinction: a model that answered in
155
153
  * prose may answer correctly next time, where a 401 will not. `spoken` is the
156
154
  * field this template exists for — without it the announced turn has nothing to
@@ -256,7 +254,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }) {
256
254
  * to completion in a worker, where the body is what may suspend. Splitting it
257
255
  * out keeps `recapFlow` readable and costs nothing, since the WDK transform
258
256
  * rewrites a step's DECLARATION rather than its call sites, so a step called
259
- * from a helper is still a real step (`mapInBatches` rests on the same
257
+ * from a helper is still a real step (`mapConcurrent` rests on the same
260
258
  * property).
261
259
  *
262
260
  * The loop is deterministic despite looking like it is not: every branch turns
@@ -358,14 +356,14 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
358
356
 
359
357
  await report(`Submitting ${new URL(url).hostname} for transcription…`);
360
358
 
361
- const response = await request(TRANSCRIPT_ENDPOINT, {
362
- method: "POST",
363
- body: JSON.stringify({ audio_url: url, speaker_labels: true }),
364
- });
365
- const body = await response.json();
366
- const id = readString(body, "id");
367
- if (!id) throw new Error("The provider accepted the recording but named no transcript id.");
368
- return { id };
359
+ // `stepTranscribeSubmitClassified` owns the endpoint, the raw-key auth, the
360
+ // PLURAL `speech_models` field and the failure classification — the
361
+ // `Classified` suffix being that last part: it is `stepTranscribeSubmit` with
362
+ // `throwStepError` already applied, so a provider refusal stays terminal and a
363
+ // rate limit waits out the delay the provider itself named. `speaker_labels`
364
+ // is this desk's own request, which is what `params` is for — the async API's
365
+ // surface is large and the SDK deliberately does not mirror it.
366
+ return await stepTranscribeSubmitClassified(url, { params: { speaker_labels: true } });
369
367
  }
370
368
 
371
369
  /**
@@ -374,6 +372,17 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
374
372
  * One poll is one step, so each attempt is journaled on its own: a run that dies
375
373
  * mid-wait resumes knowing what the last answer was instead of starting the
376
374
  * recording over.
375
+ *
376
+ * **Deliberately NOT `stepTranscribePoll`, though its sibling above did move to
377
+ * the SDK.** That helper answers `done` and THROWS on a job the provider gave
378
+ * up on, which is the right shape for a flow whose only question is "is the
379
+ * text ready". This desk's question is different: `status` is a VALUE here,
380
+ * read by the Query port (`recap_status`) while the run is still going, and an
381
+ * `error` status is the branch that unwinds the saga's compensation stack
382
+ * rather than a failure to propagate. Converting this would trade a documented
383
+ * state machine — the thing this template is actually about — for a throw.
384
+ * The provider's status union is the template's subject, so it stays in the
385
+ * template.
377
386
  */
378
387
  export async function checkTranscript(id: string): Promise<TranscriptState> {
379
388
  "use step";
@@ -456,19 +465,20 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
456
465
  throw new FatalError("That recording came back with no speech in it.");
457
466
  }
458
467
 
459
- // `stepGenerateJson` unwraps the fence a model puts around JSON however firmly
460
- // it is told not to, parses it, and validates it — all four things this step
461
- // used to re-derive. `throwStepError` is what makes a terminal gateway failure
462
- // (a bad key, a rejected request) stop rather than burn the remaining
463
- // attempts, where a reply that missed the SHAPE throws plainly and retries.
464
- const parsed = await stepGenerateJson(text, {
468
+ // `stepGenerateJsonClassified` unwraps the fence a model puts around JSON
469
+ // however firmly it is told not to, parses it, and validates it — all four
470
+ // things this step used to re-derive. The `Classified` half is what makes a
471
+ // terminal gateway failure (a bad key, a rejected request) stop rather than
472
+ // burn the remaining attempts, where a reply that missed the SHAPE throws
473
+ // plainly and retries.
474
+ const parsed = await stepGenerateJsonClassified(text, {
465
475
  schema: RecapReply,
466
476
  system:
467
477
  "You write up recordings for someone who will hear the result on a phone call. " +
468
478
  `Reply with JSON only: {"headline": string, "points": string[], "spoken": string}. ` +
469
479
  `Give exactly ${POINTS} points. "spoken" is ONE sentence, under 30 words, ` +
470
480
  "written to be read aloud. No markdown fence, no preamble.",
471
- }).catch(throwStepError);
481
+ });
472
482
 
473
483
  return {
474
484
  url,
@@ -508,27 +518,24 @@ async function request(
508
518
  url: string,
509
519
  init: { method?: string; body?: string } = {},
510
520
  ): Promise<Response> {
511
- // `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent runs (and
512
- // this workflow POLLS, so one run is many requests) get a socket each rather
513
- // than N streams on one connection — and a connection failure arrives as a
514
- // `StepTransportError` naming its cause instead of a bare
521
+ // Through `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
522
+ // runs (and this workflow POLLS, so one run is many requests) get a socket
523
+ // each rather than N streams on one connection — and a connection failure
524
+ // arrives as a `StepTransportError` naming its cause instead of a bare
515
525
  // `TypeError: fetch failed`, which for a template whose whole subject is
516
526
  // durability is the difference between a diagnosable resume and a mystery.
517
527
  // `sdk/step-fetch.ts` carries the measurements.
518
- const response = await stepFetch(url, {
528
+ // `stepFetchOk` makes the three-way retry decision: a 401 or a 400 answers the
529
+ // same way on the fourth attempt and burns the step, a 429 or a 5xx is what
530
+ // retries are for, and a `Retry-After` the provider named is waited out rather
531
+ // than replaced by the DevKit's one-second default — which matters here more
532
+ // than usual, because a fan-out of segments hits a rate limit together. The
533
+ // DELETE below stays on plain `stepFetch`, because there a 404 is a SUCCESS.
534
+ return await stepFetchOk(url, {
519
535
  ...init,
520
536
  headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
521
537
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
522
538
  });
523
- // The three-way retry decision, made by the SDK: a 401 or a 400 answers the
524
- // same way on the fourth attempt and burns the step, a 429 or a 5xx is what
525
- // retries are for, and a `Retry-After` the provider named is waited out rather
526
- // than replaced by the DevKit's one-second default — which matters here more
527
- // than usual, because a fan-out of segments hits a rate limit together.
528
- if (!response.ok) {
529
- throw toStepError(response, `${init.method ?? "GET"} ${url} failed: HTTP ${response.status}`);
530
- }
531
- return response;
532
539
  }
533
540
 
534
541
  /** A string field of a JSON body, when it really is one. */
@@ -17,6 +17,7 @@
17
17
  * of that lives.
18
18
  */
19
19
 
20
+ import { parseSchemaInput, schemaInputIssues } from "@alexkroman1/aai/testing";
20
21
  import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
21
22
  import { beforeEach, describe, expect, test, vi } from "vitest";
22
23
  import { FatalError } from "workflow";
@@ -24,7 +25,6 @@ import agentDef, { MAX_ROUNDS, redline } from "./agent.ts";
24
25
  import {
25
26
  briefBlock,
26
27
  clampScore,
27
- countWords,
28
28
  critiqueDraft,
29
29
  MAX_NOTES,
30
30
  type RedlineInput,
@@ -70,30 +70,37 @@ describe("the agent declares itself a workflow app", () => {
70
70
  });
71
71
 
72
72
  describe("the input schema", () => {
73
- const validate = (value: unknown) => redline.input?.["~standard"].validate(value);
73
+ // `schemaInputIssues` / `parseSchemaInput` rather than a local reach through
74
+ // `["~standard"].validate`: that is the vendor WIRE contract, and whether it
75
+ // answers synchronously or with a promise is the vendor's business — a missing
76
+ // `await` there leaves `.issues` undefined and every refusing test below
77
+ // passes for the wrong reason.
78
+ const issues = (value: unknown) => schemaInputIssues(redline.input, value, "redline");
74
79
 
75
80
  test("caps the rounds at the CALL SITE rather than on the bill", async () => {
76
- const tooMany = await validate({ ...INPUT, rounds: MAX_ROUNDS + 1 });
77
- expect(tooMany?.issues).toBeDefined();
81
+ expect(await issues({ ...INPUT, rounds: MAX_ROUNDS + 1 })).toBeDefined();
78
82
  });
79
83
 
80
84
  test("defaults the rounds and the required points, so the form need not", async () => {
81
- const result = await validate({ brief: INPUT.brief, audience: "engineers" });
82
- if (!result || result.issues) throw new Error("expected valid input");
83
- expect(result.value).toMatchObject({ rounds: 2, mustCover: [] });
85
+ const parsed = await parseSchemaInput(
86
+ redline.input,
87
+ { brief: INPUT.brief, audience: "engineers" },
88
+ "redline",
89
+ );
90
+ expect(parsed).toMatchObject({ rounds: 2, mustCover: [] });
84
91
  });
85
92
 
86
93
  test("rejects an audience outside the enum — which is also what makes it a select", async () => {
87
94
  // `<WorkflowFields>` renders a `z.enum` as a `<SelectField>`; the same
88
95
  // declaration is what stops an API caller inventing a fifth audience.
89
- expect((await validate({ ...INPUT, audience: "cats" }))?.issues).toBeDefined();
96
+ expect(await issues({ ...INPUT, audience: "cats" })).toBeDefined();
90
97
  });
91
98
 
92
99
  test("declares mustCover as an array, which is what the page renders by hand", async () => {
93
100
  // The mixed-form case: `<WorkflowFields>` renders scalars only, so client.tsx
94
101
  // writes this field itself and maps a textarea into it.
95
- expect((await validate({ ...INPUT, mustCover: "one point" }))?.issues).toBeDefined();
96
- expect((await validate({ ...INPUT, mustCover: ["one point"] }))?.issues).toBeUndefined();
102
+ expect(await issues({ ...INPUT, mustCover: "one point" })).toBeDefined();
103
+ expect(await issues({ ...INPUT, mustCover: ["one point"] })).toBeUndefined();
97
104
  });
98
105
  });
99
106
 
@@ -111,11 +118,6 @@ describe("pure helpers", () => {
111
118
  expect(briefBlock({ ...INPUT, mustCover: [] })).toContain("nothing specific");
112
119
  });
113
120
 
114
- test("countWords ignores surrounding and repeated whitespace", () => {
115
- expect(countWords(" one two\nthree ")).toBe(3);
116
- expect(countWords(" ")).toBe(0);
117
- });
118
-
119
121
  test("clampScore holds a model's number inside the range it was given", () => {
120
122
  expect(clampScore(12)).toBe(10);
121
123
  expect(clampScore(0)).toBe(1);
@@ -27,7 +27,8 @@
27
27
  */
28
28
 
29
29
  import "@alexkroman1/aai-ui/styles.css";
30
- import type { WorkflowOutputOf } from "@alexkroman1/aai";
30
+ import { plural } from "@alexkroman1/aai/utils";
31
+ import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
31
32
  import {
32
33
  Form,
33
34
  type FormValues,
@@ -35,6 +36,7 @@ import {
35
36
  SubmitButton,
36
37
  TextAreaField,
37
38
  useWorkflowSubmit,
39
+ WORKFLOW_STATUS_LABELS,
38
40
  WorkflowFields,
39
41
  WorkflowProgress,
40
42
  type WorkflowRun,
@@ -150,8 +152,8 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
150
152
  {run.status === "completed" && (
151
153
  <>
152
154
  <p className="text-xs opacity-60">
153
- {run.output.words} words · {run.output.roundsRun} round
154
- {run.output.roundsRun === 1 ? "" : "s"} ·{" "}
155
+ {run.output.words} words · {run.output.roundsRun}{" "}
156
+ {plural(run.output.roundsRun, "round")} ·{" "}
155
157
  {/* Which of the two stop conditions ended the loop is the one thing
156
158
  a reader cannot infer from the round count alone. */}
157
159
  {run.output.shipped ? "the critic stopped it" : "the round budget stopped it"}
@@ -170,15 +172,13 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
170
172
  /**
171
173
  * One line describing where a run has got to.
172
174
  *
173
- * A `Record` keyed by the status union rather than a switch, so a status added
174
- * to the SDK is a compile error here instead of falling through a `default:`.
175
+ * The SDK's map with the one label this desk wants differently: `running` is
176
+ * "Writing…" here because that is what the run is doing. Spreading a COMPLETE
177
+ * `Record<WorkflowRunStatus, string>` cannot drop a key, so the exhaustiveness
178
+ * the hand-written copy was written for survives — and now lives at the SDK
179
+ * boundary, where a status added upstream is one compile error rather than one
180
+ * per page.
175
181
  */
176
- const STATUS_LINE: Record<WorkflowRun["status"], string> = {
177
- pending: "Queued",
178
- running: "Writing…",
179
- completed: "Done",
180
- failed: "Failed",
181
- cancelled: "Cancelled",
182
- };
182
+ const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
183
183
 
184
184
  page({ name: "Redline", component: RedlineDesk });
@@ -31,8 +31,9 @@
31
31
  * three functions read more tidily than one.
32
32
  */
33
33
 
34
- import { throwStepError } from "@alexkroman1/aai/step-errors";
35
- import { report, stepGenerate, stepGenerateJson } from "@alexkroman1/aai/utils";
34
+ import { report } from "@alexkroman1/aai/step";
35
+ import { stepGenerateClassified, stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
36
+ import { countWords } from "@alexkroman1/aai/utils";
36
37
  import { FatalError } from "workflow";
37
38
  import { z } from "zod";
38
39
  import { CRITIC_SYSTEM, REVISER_SYSTEM, WRITER_SYSTEM } from "./prompts.ts";
@@ -145,7 +146,7 @@ export async function writeDraft(input: RedlineInput): Promise<string> {
145
146
  // `stepGenerate` already refuses an empty completion, as a RETRYABLE
146
147
  // `StepGenerateError` — which is the right answer, and one a hand-written
147
148
  // check would have to re-derive.
148
- const draft = await ask(briefBlock(input), { system: WRITER_SYSTEM });
149
+ const draft = await stepGenerateClassified(briefBlock(input), { system: WRITER_SYSTEM });
149
150
  return draft.trim();
150
151
  }
151
152
 
@@ -168,10 +169,10 @@ export async function critiqueDraft(
168
169
  // `stepGenerateJson` owns the fence, the parse, the non-object case and the
169
170
  // shape — and throws PLAINLY when any of them misses, unlike the fatal one
170
171
  // above: a model that answered with prose may well obey on the next attempt.
171
- const parsed = await stepGenerateJson(`${briefBlock(input)}\n\nThe submission:\n${draft}`, {
172
- schema: CritiqueReply,
173
- system: CRITIC_SYSTEM,
174
- }).catch(throwStepError);
172
+ const parsed = await stepGenerateJsonClassified(
173
+ `${briefBlock(input)}\n\nThe submission:\n${draft}`,
174
+ { schema: CritiqueReply, system: CRITIC_SYSTEM },
175
+ );
175
176
 
176
177
  const critique: Critique = {
177
178
  verdict: parsed.verdict,
@@ -199,7 +200,7 @@ export async function reviseDraft(
199
200
  "use step";
200
201
 
201
202
  await report(`Round ${round}: revising.`);
202
- const revised = await ask(
203
+ const revised = await stepGenerateClassified(
203
204
  [
204
205
  briefBlock(input),
205
206
  `Your current draft:\n${draft}`,
@@ -221,11 +222,6 @@ export function briefBlock(input: RedlineInput): string {
221
222
  return [`Brief: ${input.brief}`, `Audience: ${input.audience}`, must].join("\n\n");
222
223
  }
223
224
 
224
- export function countWords(text: string): number {
225
- const trimmed = text.trim();
226
- return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
227
- }
228
-
229
225
  /** Scores arrive from a model, so they arrive out of range often enough. */
230
226
  export function clampScore(score: number): number {
231
227
  if (!Number.isFinite(score)) return 0;
@@ -233,24 +229,16 @@ export function clampScore(score: number): number {
233
229
  }
234
230
 
235
231
  // ---- The model call ---------------------------------------------------------
236
-
237
- /**
238
- * `stepGenerate`, with this desk's retry POLICY on top.
239
- *
240
- * The SDK classifies the gateway's failure (`StepGenerateError.retryable`) and
241
- * stops there: whether a terminal failure should burn the step's remaining
242
- * attempts is the caller's call. `throwStepError`
243
- * (`@alexkroman1/aai/step-errors`) is that call made one way terminal stays
244
- * terminal, and a rate limit becomes a `RetryableError` carrying the delay the
245
- * gateway itself named, which beats `RetryableError`'s own one-second default.
246
- *
247
- * This desk used to carry that mapping itself, as did `research-workflow` and
248
- * `link-digest`; it is one import now, and the delay is no longer the one line
249
- * only this template remembered.
250
- */
251
- async function ask(prompt: string, opts: { system: string }): Promise<string> {
252
- return await stepGenerate(prompt, opts).catch(throwStepError);
253
- }
232
+ //
233
+ // There is no local `ask()` any more, and its absence is the point. The SDK
234
+ // classifies the gateway's failure (`StepGenerateError.retryable`) and stops
235
+ // there — whether a terminal failure should burn the step's remaining attempts
236
+ // is the caller's call — so `stepGenerateClassified` and
237
+ // `stepGenerateJsonClassified` (`@alexkroman1/aai/step-errors`) are that call
238
+ // made one way: terminal stays terminal, and a rate limit becomes a
239
+ // `RetryableError` carrying the delay the gateway itself named, which beats
240
+ // `RetryableError`'s own one-second default. Three templates each wrapped the
241
+ // raw `/step` call to say that; the wrapper is a suffix on the import now.
254
242
 
255
243
  /** A rate limit — and a model that ignored the format — are both expected. */
256
244
  critiqueDraft.maxRetries = 5;