@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
@@ -19,7 +19,7 @@
19
19
  * The first three are ordinary step work. The fourth was impossible until two
20
20
  * things existed, and they are what this template is the reference use of:
21
21
  *
22
- * - **`stepSpeak`** (`@alexkroman1/aai/utils`) synthesizes from inside a step.
22
+ * - **`stepSpeak`** (`@alexkroman1/aai/step`) synthesizes from inside a step.
23
23
  * The session TTS surface cannot be used here at all: a `TtsSession` is an
24
24
  * event stream wired into a live pipeline's playback, with a turn tracker and
25
25
  * barge-in behind it, and a step has no turn to be part of and has to return
@@ -76,8 +76,9 @@
76
76
 
77
77
  import { workflow, workflowApp } from "@alexkroman1/aai";
78
78
  import { ASSEMBLYAI_TTS_DEFAULT_VOICE, ASSEMBLYAI_TTS_VOICES } from "@alexkroman1/aai/tts";
79
+ import type { WorkflowDef } from "@alexkroman1/aai/workflow-api";
79
80
  import { z } from "zod";
80
- import { spokenSummaryFlow } from "./workflows/summarize.ts";
81
+ import { type SpokenSummary, spokenSummaryFlow } from "./workflows/summarize.ts";
81
82
 
82
83
  /**
83
84
  * The voices the form offers.
@@ -102,28 +103,40 @@ const VOICES = Object.entries(ASSEMBLYAI_TTS_VOICES)
102
103
  */
103
104
  const [FIRST_VOICE = ASSEMBLYAI_TTS_DEFAULT_VOICE, ...OTHER_VOICES] = VOICES;
104
105
 
106
+ /**
107
+ * The run input, as its own const.
108
+ *
109
+ * Named rather than inline because {@link spokenSummary} carries an explicit
110
+ * type, and that annotation is what lets `workflows/summarize.ts` name
111
+ * `WorkflowInputOf<typeof spokenSummary>` for its body's parameter: the body's
112
+ * own signature would otherwise be part of what infers this declaration's type,
113
+ * and TypeScript refuses the cycle (`TS7022`).
114
+ */
115
+ const spokenSummaryInput = z.object({
116
+ // A plain string, because an upload id is what the run really receives.
117
+ // What makes it a file picker rather than a text box is the `uploads` line
118
+ // below.
119
+ recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
120
+ // An enum, so the form renders a SELECT rather than a text box — which is
121
+ // the whole reason the list is derived above rather than left free-form.
122
+ // Optional, so the SDK's own default voice applies when nobody chooses.
123
+ voice: z
124
+ .enum([FIRST_VOICE, ...OTHER_VOICES])
125
+ .optional()
126
+ .describe("Voice to read the summary in"),
127
+ });
128
+
105
129
  /**
106
130
  * The declaration: schema, description, and the directive body.
107
131
  *
108
132
  * Exported so `WorkflowOutputOf<typeof spokenSummary>` names the output type in
109
133
  * one place — including from `client.tsx`, where `import type` is erased and so
110
- * bundles nothing server-side.
134
+ * bundles nothing server-side — and so `workflows/summarize.ts` can name
135
+ * `WorkflowInputOf<typeof spokenSummary>` for the body's parameter.
111
136
  */
112
- export const spokenSummary = workflow({
137
+ export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary> = workflow({
113
138
  description: "Transcribe a recording, summarize it, and read the summary back as audio",
114
- input: z.object({
115
- // A plain string, because an upload id is what the run really receives.
116
- // What makes it a file picker rather than a text box is the `uploads` line
117
- // below.
118
- recording: z.string().describe("A recording to summarize — WAV, MP3 or M4A"),
119
- // An enum, so the form renders a SELECT rather than a text box — which is
120
- // the whole reason the list is derived above rather than left free-form.
121
- // Optional, so the SDK's own default voice applies when nobody chooses.
122
- voice: z
123
- .enum([FIRST_VOICE, ...OTHER_VOICES])
124
- .optional()
125
- .describe("Voice to read the summary in"),
126
- }),
139
+ input: spokenSummaryInput,
127
140
  // The one line that makes the form take a file: `<WorkflowFields>` renders a
128
141
  // picker for this property, `useWorkflowSubmit` stores the chosen file, and
129
142
  // the step that transcribes it reads it back with `readUpload`.
@@ -36,18 +36,19 @@ import "@alexkroman1/aai-ui/styles.css";
36
36
  // ERASED at build time, so naming the agent's own type costs the browser bundle
37
37
  // nothing — and it is what stops this file restating a shape
38
38
  // `workflows/summarize.ts` already declares.
39
- import type { WorkflowOutputOf } from "@alexkroman1/aai";
39
+ import { formatDuration } from "@alexkroman1/aai/utils";
40
+ import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
40
41
  import {
41
42
  createWorkflowApi,
42
43
  Form,
43
44
  page,
44
45
  SubmitButton,
45
46
  UploadProgressBar,
47
+ useDownloadUrl,
46
48
  useWorkflowSubmit,
47
49
  WorkflowFields,
48
50
  WorkflowProgress,
49
51
  } from "@alexkroman1/aai-ui";
50
- import { useEffect, useState } from "react";
51
52
  import type { spokenSummary } from "./agent.ts";
52
53
 
53
54
  /** What a completed run reports, derived from the workflow rather than restated. */
@@ -84,58 +85,16 @@ function captionsUrl(text: string, durationMs: number): string {
84
85
  return `data:text/vtt;charset=utf-8,${encodeURIComponent(vtt)}`;
85
86
  }
86
87
 
87
- /** Seconds a person can read, from the milliseconds a run reports. */
88
- function duration(ms: number): string {
89
- const total = Math.round(ms / 1000);
90
- return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
91
- }
92
-
93
- /**
94
- * The finished run's audio, as something the browser will play.
95
- *
96
- * A hook rather than four lines in the component because the CLEANUP is the
97
- * part worth keeping in one place: an object URL pins its blob for the life of
98
- * the document, so it is revoked when the id changes and when the page goes
99
- * away. The `cancelled` flag covers the other half — a second run settling
100
- * while the first download is still in flight would otherwise set state from
101
- * the stale one.
102
- */
103
- function useAudioUrl(uploadId: string | undefined): { url?: string; error?: string } {
104
- const [state, setState] = useState<{ url?: string; error?: string }>({});
105
-
106
- useEffect(() => {
107
- if (uploadId === undefined) {
108
- setState({});
109
- return;
110
- }
111
- let cancelled = false;
112
- let objectUrl: string | undefined;
113
- api
114
- .download(uploadId)
115
- .then((blob) => {
116
- if (cancelled) return;
117
- objectUrl = URL.createObjectURL(blob);
118
- setState({ url: objectUrl });
119
- })
120
- .catch((err: unknown) => {
121
- if (!cancelled) setState({ error: err instanceof Error ? err.message : String(err) });
122
- });
123
- return () => {
124
- cancelled = true;
125
- if (objectUrl !== undefined) URL.revokeObjectURL(objectUrl);
126
- };
127
- }, [uploadId]);
128
-
129
- return state;
130
- }
131
-
132
88
  export function App() {
133
89
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
134
90
  // `run.output` instead of `unknown`.
135
91
  const { submit, run, pending, upload, pauseUpload, resumeUpload, error } =
136
92
  useWorkflowSubmit<Summary>(WORKFLOW, { api });
137
93
  const output = run?.status === "completed" ? run.output : undefined;
138
- const audio = useAudioUrl(output?.audio);
94
+ // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
95
+ // bytes have to be FETCHED and handed to the element as an object URL — and the
96
+ // object URL has to be revoked, which is the half a page written by hand forgets.
97
+ const audio = useDownloadUrl(output?.audio, { api });
139
98
 
140
99
  return (
141
100
  <main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
@@ -169,7 +128,7 @@ export function App() {
169
128
  <div className="flex flex-col gap-1">
170
129
  <h2 className="text-xl">{output.headline}</h2>
171
130
  <p className="text-sm opacity-70">
172
- {output.source} · {duration(output.durationMs)} · {output.words} words
131
+ {output.source} · {formatDuration(output.durationMs)} · {output.words} words
173
132
  </p>
174
133
  </div>
175
134
 
@@ -181,8 +140,9 @@ export function App() {
181
140
 
182
141
  <section className="flex flex-col gap-2">
183
142
  <h3 className="text-sm font-medium opacity-70">
184
- Read aloud · {duration(output.audioDurationMs)}
143
+ Read aloud · {formatDuration(output.audioDurationMs)}
185
144
  </h3>
145
+ {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
186
146
  {audio.error !== undefined && (
187
147
  <p className="text-red-600">Could not load the audio: {audio.error}</p>
188
148
  )}
@@ -24,7 +24,7 @@
24
24
  * run. The bytes go to the store, the output carries the id, and the page
25
25
  * turns it back into something to play with `api.download(id)`.
26
26
  *
27
- * Both are on `@alexkroman1/aai/utils`, imported from THERE rather than the
27
+ * Both are on `@alexkroman1/aai/step`, imported from THERE rather than the
28
28
  * root: a `workflows/*.ts` module is bundled separately by the WDK builder, so
29
29
  * the root barrel's module graph would ride into the step bundle.
30
30
  *
@@ -49,19 +49,17 @@
49
49
  * expensive one.
50
50
  */
51
51
 
52
- import { throwStepError } from "@alexkroman1/aai/step-errors";
53
- import {
54
- omitUndefined,
55
- report,
56
- stepGenerateJson,
57
- stepSpeak,
58
- TRANSCRIBE_API,
59
- writeUpload,
60
- } from "@alexkroman1/aai/utils";
52
+ import { report, stepSpeak, TRANSCRIBE_API, writeUpload } from "@alexkroman1/aai/step";
53
+ import { stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
54
+ import { countWords, omitUndefined } from "@alexkroman1/aai/utils";
55
+ // ERASED at build time, so the body can name the schema's own output type without
56
+ // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
57
+ // for `WorkflowOutputOf`.
58
+ import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
61
59
  import { sleep } from "workflow";
62
60
  import { z } from "zod";
61
+ import type { spokenSummary } from "../agent.ts";
63
62
  import {
64
- countWords,
65
63
  createJob,
66
64
  MAX_POLLS,
67
65
  POLL_INTERVAL,
@@ -128,13 +126,9 @@ export type SpokenSummary = {
128
126
  };
129
127
 
130
128
  /** Transcribe a recording, summarize it, and read the summary back. */
131
- export async function spokenSummaryFlow(input: {
132
- recording: string;
133
- // `| undefined` explicitly, not merely optional: `exactOptionalPropertyTypes`
134
- // is on repo-wide, and what a zod `.optional()` infers is a property that may
135
- // be PRESENT and undefined.
136
- voice?: string | undefined;
137
- }): Promise<SpokenSummary> {
129
+ export async function spokenSummaryFlow(
130
+ input: WorkflowInputOf<typeof spokenSummary>,
131
+ ): Promise<SpokenSummary> {
138
132
  "use workflow";
139
133
 
140
134
  const transcript = await transcribe(input.recording);
@@ -188,7 +182,7 @@ export async function summarize(
188
182
  "use step";
189
183
 
190
184
  await report("Summarizing the transcript.");
191
- const reply = await stepGenerateJson(
185
+ const reply = await stepGenerateJsonClassified(
192
186
  "Summarize this transcript of a recording.\n\n" +
193
187
  "Answer with JSON only, in this shape:\n" +
194
188
  `{"headline": "...", "points": ["..."], "spoken": "..."}\n\n` +
@@ -204,9 +198,10 @@ export async function summarize(
204
198
  system: "You summarize recordings. You answer with JSON and nothing else.",
205
199
  schema: SummaryReply,
206
200
  },
207
- // Classified off the gateway's own status: a 429 is worth another attempt
208
- // and a 400 is not, and `throwStepError` is what tells the DevKit which.
209
- ).catch(throwStepError);
201
+ // The `Classified` caller is `stepGenerateJson` plus `throwStepError`, which
202
+ // reads the gateway's own status: a 429 is worth another attempt and a 400
203
+ // is not, and that is what tells the DevKit which.
204
+ );
210
205
 
211
206
  return { headline: reply.headline, points: reply.points.slice(0, POINTS), spoken: reply.spoken };
212
207
  }
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * **Every one of them is four lines, because the SDK owns the endpoint.**
14
14
  * `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
15
- * `@alexkroman1/aai/utils` carry the URL, the raw-key auth, the windowed
15
+ * `@alexkroman1/aai/step` reached here through their `*Classified` callers on
16
+ * `@alexkroman1/aai/step-errors` — carry the URL, the raw-key auth, the windowed
16
17
  * streaming upload, the PLURAL `speech_models` field and the failure
17
18
  * classification — all of which this file used to spell out, and all of which
18
19
  * `transcription-workflow` used to spell out again, differently worded and
@@ -37,14 +38,13 @@
37
38
  * the one leg that should be as boring as possible.
38
39
  */
39
40
 
40
- import { throwStepError } from "@alexkroman1/aai/step-errors";
41
+ import { report, uploadInfo } from "@alexkroman1/aai/step";
41
42
  import {
42
- report,
43
- stepTranscribePoll,
44
- stepTranscribeSubmit,
45
- stepTranscribeUpload,
46
- uploadInfo,
47
- } from "@alexkroman1/aai/utils";
43
+ stepTranscribePollClassified,
44
+ stepTranscribeSubmitClassified,
45
+ stepTranscribeUploadClassified,
46
+ } from "@alexkroman1/aai/step-errors";
47
+ import { countWords, formatBytes } from "@alexkroman1/aai/utils";
48
48
 
49
49
  /** How long between polls of a submitted job. */
50
50
  export const POLL_INTERVAL = "10s";
@@ -79,16 +79,20 @@ export type Transcript = {
79
79
  * expires before the next step runs; that costs one fresh upload, once, instead
80
80
  * of five.
81
81
  *
82
- * `.catch(throwStepError)` is what turns the SDK's `TranscribeError` into the
83
- * DevKit's verdict a missing key and a 400 stop, a 429 waits as long as the
84
- * service asked. Every step here ends the same way for the same reason.
82
+ * The `Classified` callers on `@alexkroman1/aai/step-errors` are the SDK's own
83
+ * `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns
84
+ * the SDK's `TranscribeError` into the DevKit's verdict a missing key and a
85
+ * 400 stop, a 429 waits as long as the service asked. Every step here ends the
86
+ * same way for the same reason.
85
87
  */
86
88
  export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
87
89
  "use step";
88
90
 
89
91
  const stored = await uploadInfo(uploadId);
90
- await report(`Uploading ${stored.name || uploadId} (${mb(stored.size)}) for transcription.`);
91
- return await stepTranscribeUpload(uploadId).catch(throwStepError);
92
+ await report(
93
+ `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
94
+ );
95
+ return await stepTranscribeUploadClassified(uploadId);
92
96
  }
93
97
 
94
98
  /** Retries beyond the default 3: an upload is the one call here worth another attempt. */
@@ -98,7 +102,7 @@ uploadToProvider.maxRetries = 5;
98
102
  export async function createJob(audioUrl: string): Promise<{ id: string }> {
99
103
  "use step";
100
104
 
101
- const job = await stepTranscribeSubmit(audioUrl).catch(throwStepError);
105
+ const job = await stepTranscribeSubmitClassified(audioUrl);
102
106
  await report(`Transcribing — job ${job.id}.`);
103
107
  return job;
104
108
  }
@@ -118,7 +122,7 @@ export async function pollTranscript(
118
122
  ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
119
123
  "use step";
120
124
 
121
- const progress = await stepTranscribePoll(id).catch(throwStepError);
125
+ const progress = await stepTranscribePollClassified(id);
122
126
  if (!progress.done) return { done: false };
123
127
 
124
128
  const stored = await uploadInfo(uploadId);
@@ -132,14 +136,3 @@ export async function pollTranscript(
132
136
  },
133
137
  };
134
138
  }
135
-
136
- /** Words in a transcript, for the counts a page shows. */
137
- export function countWords(text: string): number {
138
- const trimmed = text.trim();
139
- return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
140
- }
141
-
142
- /** A size a person can read, because the number that matters is the scale. */
143
- function mb(bytes: number): string {
144
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
145
- }
@@ -1,10 +1,9 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
4
3
  import {
5
4
  createToolContext,
6
- runTool,
7
5
  stubGenerate,
6
+ toolRunner,
8
7
  withDiscoveredTools,
9
8
  } from "@alexkroman1/aai/testing";
10
9
  import { describe, expect, test } from "vitest";
@@ -23,7 +22,7 @@ const agentDef = withDiscoveredTools(
23
22
  import.meta.glob("./tools/*.ts", { eager: true }),
24
23
  );
25
24
 
26
- import { MAX_ATTEMPTS, runCorrectiveRag } from "./graph.ts";
25
+ import { MAX_ATTEMPTS, runCorrectiveRag } from "./procedure.ts";
27
26
  import {
28
27
  ANSWER_SYSTEM,
29
28
  ANSWERS_SYSTEM,
@@ -95,15 +94,11 @@ function scriptedModel(script: Script = {}) {
95
94
  return { generate, calls };
96
95
  }
97
96
 
98
- function makeCtx(generate: GenerateFn, sessionId?: string) {
99
- return createToolContext({ generate, ...(sessionId ? { sessionId } : {}) });
100
- }
101
-
102
- /** A tool by the name the model calls it by, bound to this agent. The lookup
103
- * and its "no such tool" message are `runTool`'s (`@alexkroman1/aai/testing`);
104
- * what is local is only which agent they run against. */
105
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
106
- runTool(agentDef, name, args, ctx);
97
+ /** A tool by the name the model calls it by, bound to this agent. The lookup,
98
+ * its "no such tool" message and the args-or-context shape are all
99
+ * `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
100
+ * agent it runs against. */
101
+ const run = toolRunner(agentDef);
107
102
 
108
103
  /** Node names without the per-call suffix, for sequence assertions. */
109
104
  function nodes(calls: string[]): string[] {
@@ -254,7 +249,7 @@ describe("answer_question", () => {
254
249
  relevant: (id) => id === "D8",
255
250
  answers: ["Area outages are on the status page, and rebooting will not help."],
256
251
  });
257
- const ctx = makeCtx(generate);
252
+ const ctx = createToolContext({ generate });
258
253
  const result = (await run("answer_question", { question: "is there an outage" }, ctx)) as {
259
254
  answer: string;
260
255
  sources: string[];
@@ -276,7 +271,7 @@ describe("answer_question", () => {
276
271
 
277
272
  test("with nothing grounded it returns no answer and points at the ticket", async () => {
278
273
  const { generate } = scriptedModel({ relevant: () => false });
279
- const ctx = makeCtx(generate);
274
+ const ctx = createToolContext({ generate });
280
275
  const result = (await run("answer_question", { question: "do you sell phones" }, ctx)) as {
281
276
  answer: null;
282
277
  guidance: string;
@@ -302,8 +297,8 @@ describe("answer_question", () => {
302
297
  // session ids would prove nothing extra, and `sessionSlot` could stop
303
298
  // keying by session with this still passing.
304
299
  const { generate } = scriptedModel({ relevant: (id) => id === "D2", answers: ["Reboot it."] });
305
- const first = makeCtx(generate);
306
- const second = makeCtx(generate);
300
+ const first = createToolContext({ generate });
301
+ const second = createToolContext({ generate });
307
302
 
308
303
  await run("answer_question", { question: "how do I reboot" }, first);
309
304
  expect(supportSlot.get(second).trace).toBeNull();
@@ -3,7 +3,7 @@ import { PRODUCT, supportProjection } from "./shared.ts";
3
3
 
4
4
  /**
5
5
  * A support line that grades its own retrieval before it speaks — the
6
- * self-RAG / CRAG graph, ported to voice. `graph.ts` holds the loop, `prompts.ts`
6
+ * self-RAG / CRAG graph, ported to voice. `procedure.ts` holds the loop, `prompts.ts`
7
7
  * the attribution and the graders, `shared.ts` the knowledge base and why its
8
8
  * retriever is lexical.
9
9
  *
@@ -1,5 +1,5 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import { AutoScroll, client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
2
+ import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
3
3
  import { PRODUCT, supportProjection } from "./shared.ts";
4
4
 
5
5
  /**
@@ -11,12 +11,11 @@ import { PRODUCT, supportProjection } from "./shared.ts";
11
11
  * support line nobody can tell apart from one that just guesses well.
12
12
  */
13
13
  function TraceSidebar() {
14
- const theme = useTheme();
15
14
  const support = useAgentState(supportProjection);
16
15
  const trace = support.trace;
17
16
 
18
17
  return (
19
- <div className="flex h-full min-h-0 flex-col gap-4 p-4" style={{ color: theme.text }}>
18
+ <div className="flex h-full min-h-0 flex-col gap-4 p-4 text-aai-text">
20
19
  <div>
21
20
  <h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
22
21
  <p className="text-xs opacity-50">
@@ -36,7 +35,7 @@ function TraceSidebar() {
36
35
  scrollClassName="min-h-0 overflow-y-auto"
37
36
  contentClassName="flex flex-col gap-4 pr-1"
38
37
  >
39
- <div className="rounded-lg p-3" style={{ background: theme.surface }}>
38
+ <div className="rounded-lg p-3 bg-aai-surface">
40
39
  <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Question</p>
41
40
  <p className="text-sm">{trace.question}</p>
42
41
  {trace.rewrites > 0 && (
@@ -54,10 +53,11 @@ function TraceSidebar() {
54
53
  {trace.docs.map((doc) => (
55
54
  <div
56
55
  key={doc.id}
57
- className="flex items-start gap-2 rounded-lg p-2"
58
- style={{ background: theme.surface, opacity: doc.relevant ? 1 : 0.5 }}
56
+ className={`flex items-start gap-2 rounded-lg p-2 bg-aai-surface ${
57
+ doc.relevant ? "" : "opacity-50"
58
+ }`}
59
59
  >
60
- <span style={{ color: doc.relevant ? theme.primary : theme.text }}>
60
+ <span className={doc.relevant ? "text-aai-primary" : "text-aai-text"}>
61
61
  {doc.relevant ? "✓" : "×"}
62
62
  </span>
63
63
  <div className="min-w-0">
@@ -69,7 +69,7 @@ function TraceSidebar() {
69
69
  </div>
70
70
 
71
71
  <div className="flex flex-col gap-1">
72
- <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Graph</p>
72
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Procedure</p>
73
73
  {trace.steps.map((entry, index) => (
74
74
  // Steps are append-only within a run, so the index is stable.
75
75
  <p key={`${index}-${entry.node}`} className="text-xs">
@@ -79,7 +79,7 @@ function TraceSidebar() {
79
79
  ))}
80
80
  </div>
81
81
 
82
- <div className="rounded-lg p-3" style={{ background: theme.surface }}>
82
+ <div className="rounded-lg p-3 bg-aai-surface">
83
83
  <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Verdict</p>
84
84
  {trace.answer ? (
85
85
  <p className="text-sm">{trace.answer}</p>
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The CRAG nodes' BODIES — one model call each, and nothing about routing.
3
+ *
4
+ * Split from `procedure.ts` when that file became an actual machine: the machine
5
+ * there declares which node runs next, and these are what each node does. The
6
+ * seam is worth having beyond the line count — everything here is a plain async
7
+ * function of its arguments, so a spec can drive one directly, and the machine
8
+ * holds no prompt text at all.
9
+ */
10
+
11
+ import type { GenerateFn } from "@alexkroman1/aai";
12
+ import {
13
+ ANSWER_SYSTEM,
14
+ ANSWERS_SYSTEM,
15
+ binaryScore,
16
+ DOC_GRADER_SYSTEM,
17
+ GROUNDED_SYSTEM,
18
+ REWRITE_SYSTEM,
19
+ } from "./prompts.ts";
20
+ import type { Doc, GradedDoc } from "./shared.ts";
21
+ import { formatDoc } from "./shared.ts";
22
+
23
+ /** A closed verdict from one of the graders. */
24
+ export interface Verdict {
25
+ pass: boolean;
26
+ reason: string;
27
+ }
28
+
29
+ /** `grade_documents` — one closed question per document, all at once. */
30
+ export async function gradeDocuments(
31
+ generate: GenerateFn,
32
+ question: string,
33
+ docs: readonly Doc[],
34
+ ): Promise<GradedDoc[]> {
35
+ return await Promise.all(
36
+ docs.map(async (doc) => {
37
+ const { object } = await generate({
38
+ system: DOC_GRADER_SYSTEM,
39
+ prompt: `Caller's question: ${question}\n\nDocument:\n${formatDoc(doc)}`,
40
+ schema: binaryScore,
41
+ });
42
+ return {
43
+ id: doc.id,
44
+ title: doc.title,
45
+ relevant: object.score === "yes",
46
+ reason: object.reason,
47
+ };
48
+ }),
49
+ );
50
+ }
51
+
52
+ /** `transform_query` — their question re-writer, aimed at a keyword index. */
53
+ export async function transformQuery(generate: GenerateFn, question: string): Promise<string> {
54
+ const { text } = await generate({
55
+ system: REWRITE_SYSTEM,
56
+ prompt: `Caller's question: ${question}`,
57
+ });
58
+ const rewritten = text.trim().replace(/^["']|["']$/g, "");
59
+ // A rewriter that returns nothing usable must not empty the query — that
60
+ // would retrieve zero documents and read as "we have nothing on this".
61
+ return rewritten.length > 0 ? rewritten : question;
62
+ }
63
+
64
+ /** `generate` — the answer, from the graded-relevant documents only. */
65
+ export async function generateAnswer(
66
+ generate: GenerateFn,
67
+ question: string,
68
+ docs: readonly Doc[],
69
+ ): Promise<string> {
70
+ const { text } = await generate({
71
+ system: ANSWER_SYSTEM,
72
+ prompt: `Documents:\n\n${docs.map(formatDoc).join("\n\n")}\n\nCaller's question: ${question}`,
73
+ });
74
+ return text.trim();
75
+ }
76
+
77
+ /** The two generation graders — same closed question, different subject. */
78
+ async function grade(generate: GenerateFn, system: string, prompt: string): Promise<Verdict> {
79
+ const { object } = await generate({ system, prompt, schema: binaryScore });
80
+ return { pass: object.score === "yes", reason: object.reason };
81
+ }
82
+
83
+ /** `grade_generation_v_documents` — is the answer IN the documents? */
84
+ export async function gradeGrounded(
85
+ generate: GenerateFn,
86
+ docs: readonly Doc[],
87
+ answer: string,
88
+ ): Promise<Verdict> {
89
+ const facts = docs.map(formatDoc).join("\n\n");
90
+ return await grade(generate, GROUNDED_SYSTEM, `Facts:\n\n${facts}\n\nAnswer: ${answer}`);
91
+ }
92
+
93
+ /** `grade_generation_v_question` — grounded is not the same as useful. */
94
+ export async function gradeUseful(
95
+ generate: GenerateFn,
96
+ question: string,
97
+ answer: string,
98
+ ): Promise<Verdict> {
99
+ return await grade(generate, ANSWERS_SYSTEM, `Question: ${question}\n\nAnswer: ${answer}`);
100
+ }