@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
@@ -0,0 +1,138 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * The first leg: the uploaded recording becomes text.
4
+ *
5
+ * Three steps and a durable wait, over AssemblyAI's ASYNC transcription API:
6
+ *
7
+ * ```text
8
+ * uploadToProvider one step → the file, streamed, and the URL it answered
9
+ * createJob one step → the transcript id
10
+ * pollTranscript one step + a durable sleep, until the text comes back
11
+ * ```
12
+ *
13
+ * **Every one of them is four lines, because the SDK owns the endpoint.**
14
+ * `stepTranscribeUpload` / `stepTranscribeSubmit` / `stepTranscribePoll` on
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
17
+ * streaming upload, the PLURAL `speech_models` field and the failure
18
+ * classification — all of which this file used to spell out, and all of which
19
+ * `transcription-workflow` used to spell out again, differently worded and
20
+ * identical in behaviour. What is left here is what is genuinely this app's:
21
+ * which steps to cut the job into, how long to wait, and what to report.
22
+ *
23
+ * ## The steps are still OURS, and they have to be
24
+ *
25
+ * The SDK cannot ship a `"use step"`: the Workflow DevKit's builder transforms
26
+ * exactly the files under this `workflows/` directory, so a directive inside a
27
+ * dependency would be transformed by nothing and would run inline with no
28
+ * journal and no retry, silently. The SDK owns what happens INSIDE a step; the
29
+ * boundaries — which is to say, what gets journaled and what a retry repeats —
30
+ * are the app's.
31
+ *
32
+ * **The async API rather than the sync one, and the choice is about the FORM.**
33
+ * The sync endpoint (`stepTranscribeSync`) answers inside the request and pays
34
+ * for it with a hard 120-second, 40 MB cap, so a longer recording has to be cut
35
+ * into segments and fanned out — which is a whole subject, and it has a
36
+ * template (`transcription-workflow`, which shows that cut three ways and
37
+ * measures them). This app's subject is the ROUND TRIP, so the transcription is
38
+ * the one leg that should be as boring as possible.
39
+ */
40
+
41
+ import { report, uploadInfo } from "@alexkroman1/aai/step";
42
+ import {
43
+ stepTranscribePollClassified,
44
+ stepTranscribeSubmitClassified,
45
+ stepTranscribeUploadClassified,
46
+ } from "@alexkroman1/aai/step-errors";
47
+ import { countWords, formatBytes } from "@alexkroman1/aai/utils";
48
+
49
+ /** How long between polls of a submitted job. */
50
+ export const POLL_INTERVAL = "10s";
51
+
52
+ /**
53
+ * Polls before the run gives up on a job.
54
+ *
55
+ * At {@link POLL_INTERVAL} this is an hour, well past what the async API takes
56
+ * for any recording it accepts. Bounded rather than endless because a job that
57
+ * never leaves `queued` is a run that would otherwise be replayed forever.
58
+ */
59
+ export const MAX_POLLS = 360;
60
+
61
+ /** What the first leg hands the second. */
62
+ export type Transcript = {
63
+ /** The FILENAME the uploader gave, not the opaque id — this reaches the page. */
64
+ source: string;
65
+ /** The provider's own measurement of the recording, in milliseconds. */
66
+ durationMs: number;
67
+ /** What was said. */
68
+ text: string;
69
+ };
70
+
71
+ /**
72
+ * Upload the recording to the provider and answer with the URL it gave.
73
+ *
74
+ * Its OWN step, and that is a measurement rather than a preference: folded into
75
+ * the submit, a fault in the JSON body — a deprecated field, a bad model name —
76
+ * makes the DevKit retry the whole step and re-upload the recording on every
77
+ * attempt. A retry that repeats the expensive half to fix the cheap half is not
78
+ * a retry. The `upload_url` is short-lived, so the risk being taken is that it
79
+ * expires before the next step runs; that costs one fresh upload, once, instead
80
+ * of five.
81
+ *
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.
87
+ */
88
+ export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
89
+ "use step";
90
+
91
+ const stored = await uploadInfo(uploadId);
92
+ await report(
93
+ `Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) for transcription.`,
94
+ );
95
+ return await stepTranscribeUploadClassified(uploadId);
96
+ }
97
+
98
+ /** Retries beyond the default 3: an upload is the one call here worth another attempt. */
99
+ uploadToProvider.maxRetries = 5;
100
+
101
+ /** Create the transcription job, and answer with the id that outlives this run. */
102
+ export async function createJob(audioUrl: string): Promise<{ id: string }> {
103
+ "use step";
104
+
105
+ const job = await stepTranscribeSubmitClassified(audioUrl);
106
+ await report(`Transcribing — job ${job.id}.`);
107
+ return job;
108
+ }
109
+
110
+ /**
111
+ * Ask once whether the job has finished, and read it when it has.
112
+ *
113
+ * One request answers both, which is the SDK's doing and worth knowing: this
114
+ * used to poll for a status and then fetch the identical URL a second time for
115
+ * the text. The body still branches on `done` rather than on a status string —
116
+ * a provider's vocabulary must not be interpreted in a body, where a new status
117
+ * would read as "not finished yet" forever.
118
+ */
119
+ export async function pollTranscript(
120
+ uploadId: string,
121
+ id: string,
122
+ ): Promise<{ done: false } | { done: true; transcript: Transcript }> {
123
+ "use step";
124
+
125
+ const progress = await stepTranscribePollClassified(id);
126
+ if (!progress.done) return { done: false };
127
+
128
+ const stored = await uploadInfo(uploadId);
129
+ await report(`Transcribed ${countWords(progress.transcript.text)} words.`);
130
+ return {
131
+ done: true,
132
+ transcript: {
133
+ source: stored.name || uploadId,
134
+ durationMs: progress.transcript.durationMs,
135
+ text: progress.transcript.text,
136
+ },
137
+ };
138
+ }
@@ -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
+ }