@alexkroman1/aai-cli 5.14.0 → 6.1.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 (231) hide show
  1. package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
  2. package/dist/_agent.d.ts +18 -0
  3. package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
  4. package/dist/_api-client.d.ts +39 -0
  5. package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
  6. package/dist/_bundler.d.ts +43 -5
  7. package/dist/_cli-common.d.ts +67 -3
  8. package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
  9. package/dist/_deploy.d.ts +2 -5
  10. package/dist/_dev-restart.d.ts +6 -1
  11. package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
  12. package/dist/_dev-server.d.ts +30 -2
  13. package/dist/_e2e-test-utils.d.ts +80 -0
  14. package/dist/_fault-mode.d.ts +175 -0
  15. package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
  16. package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
  17. package/dist/_output.d.ts +9 -2
  18. package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
  19. package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
  20. package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
  21. package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
  22. package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
  23. package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
  24. package/dist/_utils.d.ts +18 -2
  25. package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
  26. package/dist/_vite-env.d.ts +30 -0
  27. package/dist/build-BO1Ni6oJ.mjs +62 -0
  28. package/dist/build.d.ts +19 -0
  29. package/dist/cli-workflow.d.ts +10 -0
  30. package/dist/cli.d.ts +1 -2
  31. package/dist/cli.mjs +406 -224
  32. package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
  33. package/dist/client-bundler.mjs +1 -1
  34. package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
  35. package/dist/delete.d.ts +2 -8
  36. package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
  37. package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
  38. package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
  39. package/dist/eject.d.ts +30 -3
  40. package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
  41. package/dist/init.d.ts +11 -0
  42. package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
  43. package/dist/project-config.mjs +1 -1
  44. package/dist/scaffold/CLAUDE.md +498 -91
  45. package/dist/scaffold/package.json +11 -8
  46. package/dist/scaffold/pnpm-workspace.yaml +8 -0
  47. package/dist/scaffold/server.mjs +53 -57
  48. package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
  49. package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
  50. package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
  51. package/dist/templates/code-interpreter/agent.ts +0 -2
  52. package/dist/templates/dispatch-center/agent.test.ts +17 -19
  53. package/dist/templates/dispatch-center/agent.ts +2 -29
  54. package/dist/templates/dispatch-center/client.tsx +33 -16
  55. package/dist/templates/dispatch-center/shared.ts +53 -19
  56. package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
  57. package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
  58. package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
  59. package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
  60. package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
  61. package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
  62. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
  63. package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
  66. package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
  67. package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
  68. package/dist/templates/embedded-assets/agent.test.ts +83 -0
  69. package/dist/templates/embedded-assets/agent.ts +1 -48
  70. package/dist/templates/embedded-assets/shared.ts +17 -0
  71. package/dist/templates/embedded-assets/system-prompt.md +13 -0
  72. package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
  73. package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
  74. package/dist/templates/health-assistant/agent.test.ts +169 -0
  75. package/dist/templates/health-assistant/agent.ts +1 -166
  76. package/dist/templates/health-assistant/fda.ts +100 -0
  77. package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
  78. package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
  79. package/dist/templates/infocom-adventure/agent.test.ts +205 -0
  80. package/dist/templates/infocom-adventure/agent.ts +3 -106
  81. package/dist/templates/infocom-adventure/client.tsx +28 -12
  82. package/dist/templates/infocom-adventure/shared.ts +28 -11
  83. package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
  84. package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
  85. package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
  86. package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
  87. package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
  88. package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
  89. package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
  90. package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
  91. package/dist/templates/link-digest/agent.test.ts +223 -0
  92. package/dist/templates/link-digest/agent.ts +79 -0
  93. package/dist/templates/link-digest/client.tsx +146 -0
  94. package/dist/templates/link-digest/workflows/digest.ts +248 -0
  95. package/dist/templates/math-buddy/agent.ts +0 -2
  96. package/dist/templates/night-owl/agent.test.ts +80 -0
  97. package/dist/templates/night-owl/agent.ts +1 -68
  98. package/dist/templates/night-owl/system-prompt.md +10 -0
  99. package/dist/templates/night-owl/tools/recommend.ts +64 -0
  100. package/dist/templates/personal-finance/agent.ts +0 -2
  101. package/dist/templates/pizza-ordering/agent.test.ts +52 -47
  102. package/dist/templates/pizza-ordering/agent.ts +9 -158
  103. package/dist/templates/pizza-ordering/shared.ts +35 -12
  104. package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
  105. package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
  106. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
  107. package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
  108. package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
  109. package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
  110. package/dist/templates/plan-and-execute/agent.test.ts +411 -0
  111. package/dist/templates/plan-and-execute/agent.ts +23 -0
  112. package/dist/templates/plan-and-execute/client.tsx +115 -0
  113. package/dist/templates/plan-and-execute/graph.ts +178 -0
  114. package/dist/templates/plan-and-execute/prompts.ts +110 -0
  115. package/dist/templates/plan-and-execute/shared.ts +161 -0
  116. package/dist/templates/plan-and-execute/system-prompt.md +18 -0
  117. package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
  118. package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
  119. package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
  120. package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
  121. package/dist/templates/recap-workflow/agent.test.ts +762 -0
  122. package/dist/templates/recap-workflow/agent.ts +95 -0
  123. package/dist/templates/recap-workflow/shared.ts +43 -0
  124. package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
  125. package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
  126. package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
  127. package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
  128. package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
  129. package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
  130. package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
  131. package/dist/templates/redline/agent.test.ts +222 -0
  132. package/dist/templates/redline/agent.ts +89 -0
  133. package/dist/templates/redline/client.tsx +184 -0
  134. package/dist/templates/redline/workflows/prompts.ts +68 -0
  135. package/dist/templates/redline/workflows/redline.ts +258 -0
  136. package/dist/templates/research-workflow/agent.test.ts +525 -0
  137. package/dist/templates/research-workflow/agent.ts +94 -0
  138. package/dist/templates/research-workflow/shared.ts +35 -0
  139. package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
  140. package/dist/templates/research-workflow/tools/request_research.ts +33 -0
  141. package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
  142. package/dist/templates/research-workflow/tools/research_status.ts +40 -0
  143. package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
  144. package/dist/templates/research-workflow/workflows/research.ts +558 -0
  145. package/dist/templates/retail/agent.test.ts +236 -172
  146. package/dist/templates/retail/agent.ts +5 -42
  147. package/dist/templates/retail/client.tsx +33 -11
  148. package/dist/templates/retail/registry.test.ts +31 -16
  149. package/dist/templates/retail/resolve.ts +28 -64
  150. package/dist/templates/retail/shared.test.ts +4 -3
  151. package/dist/templates/retail/shared.ts +24 -7
  152. package/dist/templates/retail/store.test.ts +12 -10
  153. package/dist/templates/retail/store.ts +69 -30
  154. package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
  155. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
  156. package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
  157. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
  158. package/dist/templates/retail/tools/get_item_details.ts +5 -7
  159. package/dist/templates/retail/tools/get_order_details.ts +5 -7
  160. package/dist/templates/retail/tools/get_product_details.ts +4 -7
  161. package/dist/templates/retail/tools/get_user_details.ts +4 -7
  162. package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
  163. package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
  164. package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
  165. package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
  166. package/dist/templates/retail/tools/modify_user_address.ts +4 -9
  167. package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
  168. package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
  169. package/dist/templates/solo-rpg/agent.test.ts +152 -29
  170. package/dist/templates/solo-rpg/agent.ts +3 -21
  171. package/dist/templates/solo-rpg/client.tsx +19 -6
  172. package/dist/templates/solo-rpg/shared.ts +19 -11
  173. package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
  174. package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
  175. package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
  176. package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
  177. package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
  178. package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
  179. package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
  180. package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
  181. package/dist/templates/support-line/agent.test.ts +343 -0
  182. package/dist/templates/support-line/agent.ts +27 -0
  183. package/dist/templates/support-line/client.tsx +122 -0
  184. package/dist/templates/support-line/graph.ts +224 -0
  185. package/dist/templates/support-line/knowledge.json +65 -0
  186. package/dist/templates/support-line/prompts.ts +95 -0
  187. package/dist/templates/support-line/shared.ts +240 -0
  188. package/dist/templates/support-line/system-prompt.md +17 -0
  189. package/dist/templates/support-line/tools/answer_question.ts +72 -0
  190. package/dist/templates/support-line/tools/list_topics.ts +22 -0
  191. package/dist/templates/support-line/tools/log_ticket.ts +30 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +563 -0
  193. package/dist/templates/transcription-workflow/agent.ts +92 -0
  194. package/dist/templates/transcription-workflow/client.tsx +234 -0
  195. package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
  196. package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
  197. package/dist/templates/travel-concierge/agent.test.ts +338 -0
  198. package/dist/templates/travel-concierge/agent.ts +29 -0
  199. package/dist/templates/travel-concierge/client.tsx +161 -0
  200. package/dist/templates/travel-concierge/routing.ts +138 -0
  201. package/dist/templates/travel-concierge/shared.ts +547 -0
  202. package/dist/templates/travel-concierge/system-prompt.md +24 -0
  203. package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
  204. package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
  205. package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
  206. package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
  207. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
  208. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
  209. package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
  210. package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
  211. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
  212. package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
  213. package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
  214. package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
  215. package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
  216. package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
  217. package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
  218. package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
  219. package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
  220. package/dist/templates/web-researcher/agent.ts +0 -2
  221. package/dist/templates/web-researcher/system-prompt.md +10 -0
  222. package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
  223. package/dist/typecheck.mjs +17 -15
  224. package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
  225. package/dist/worker-bundler.d.ts +14 -0
  226. package/dist/worker-bundler.mjs +1 -108
  227. package/dist/workflow-B-O97AqI.mjs +125 -0
  228. package/dist/workflow-bundler.d.ts +133 -0
  229. package/dist/workflow.d.ts +67 -0
  230. package/package.json +13 -9
  231. package/dist/build-DQJ1ZSm9.mjs +0 -39
@@ -0,0 +1,343 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import type { GenerateFn, ToolContext } from "@alexkroman1/aai";
4
+ import {
5
+ createToolContext,
6
+ runTool,
7
+ stubGenerate,
8
+ withDiscoveredTools,
9
+ } from "@alexkroman1/aai/testing";
10
+ import { describe, expect, test } from "vitest";
11
+ import authoredAgent from "./agent.ts";
12
+
13
+ /**
14
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
15
+ *
16
+ * The glob is written HERE rather than reached for from a shared helper because
17
+ * this file SHIPS: it is what a scaffolded project runs, so it may not import
18
+ * anything outside its own template, and `import.meta.glob` is expanded against
19
+ * the file containing it either way. This is the pattern a user writes.
20
+ */
21
+ const agentDef = withDiscoveredTools(
22
+ authoredAgent,
23
+ import.meta.glob("./tools/*.ts", { eager: true }),
24
+ );
25
+
26
+ import { MAX_ATTEMPTS, runCorrectiveRag } from "./graph.ts";
27
+ import {
28
+ ANSWER_SYSTEM,
29
+ ANSWERS_SYSTEM,
30
+ DOC_GRADER_SYSTEM,
31
+ GROUNDED_SYSTEM,
32
+ REWRITE_SYSTEM,
33
+ } from "./prompts.ts";
34
+ import { retrieve, supportSlot, supportView } from "./shared.ts";
35
+
36
+ // ─── A scripted model ────────────────────────────────────────────────────────
37
+ //
38
+ // Every node in the graph is one `ctx.generate` call, and each call is
39
+ // identified by the system prompt it carries — so a fake that switches on
40
+ // `options.system` can drive the graph down any path, deterministically, with
41
+ // no model and no network. `calls` is the transcript, which is what the routing
42
+ // assertions read: the point of these tests is WHICH nodes ran, not what a
43
+ // model would have said.
44
+
45
+ interface Script {
46
+ /** Whether the grader passes a document, per retrieval attempt (1-based). */
47
+ relevant?: (docId: string, attempt: number) => boolean;
48
+ /** What `transform_query` rewrites to. */
49
+ rewrite?: string;
50
+ /** Answers, consumed one per `generate` node. */
51
+ answers?: string[];
52
+ /** Verdicts, consumed one per grader call. Default: pass. */
53
+ grounded?: ("yes" | "no")[];
54
+ useful?: ("yes" | "no")[];
55
+ }
56
+
57
+ function scriptedModel(script: Script = {}) {
58
+ // The NODE transcript, which is this template's own and is why the routes
59
+ // below are functions: `stubGenerate` records the calls, and what these
60
+ // assertions need is the graph's node names, labelled with their verdicts.
61
+ const calls: string[] = [];
62
+ let attempt = 1;
63
+ const answers = [...(script.answers ?? [])];
64
+ const grounded = [...(script.grounded ?? [])];
65
+ const useful = [...(script.useful ?? [])];
66
+
67
+ const { generate } = stubGenerate({
68
+ [DOC_GRADER_SYSTEM]: (call) => {
69
+ const id = /\[(D\d+)\]/.exec(call.prompt)?.[1] ?? "?";
70
+ calls.push(`grade_documents:${id}`);
71
+ const pass = script.relevant ? script.relevant(id, attempt) : true;
72
+ return { object: { score: pass ? "yes" : "no", reason: `graded ${id}` } };
73
+ },
74
+ [REWRITE_SYSTEM]: () => {
75
+ calls.push("transform_query");
76
+ attempt++;
77
+ return script.rewrite ?? "rewritten query";
78
+ },
79
+ [ANSWER_SYSTEM]: () => {
80
+ calls.push("generate");
81
+ return answers.shift() ?? "The documented answer.";
82
+ },
83
+ [GROUNDED_SYSTEM]: () => {
84
+ const verdict = grounded.shift() ?? "yes";
85
+ calls.push(`grade_generation_v_documents:${verdict}`);
86
+ return { object: { score: verdict, reason: "grounded verdict" } };
87
+ },
88
+ [ANSWERS_SYSTEM]: () => {
89
+ const verdict = useful.shift() ?? "yes";
90
+ calls.push(`grade_generation_v_question:${verdict}`);
91
+ return { object: { score: verdict, reason: "usefulness verdict" } };
92
+ },
93
+ });
94
+
95
+ return { generate, calls };
96
+ }
97
+
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);
107
+
108
+ /** Node names without the per-call suffix, for sequence assertions. */
109
+ function nodes(calls: string[]): string[] {
110
+ return calls.map((call) => call.split(":")[0] ?? call);
111
+ }
112
+
113
+ /** The same sequence with consecutive repeats collapsed — `grade_documents`
114
+ * runs once per retrieved document, and the count is `RETRIEVE_K`'s business
115
+ * rather than the routing's. */
116
+ function phases(calls: string[]): string[] {
117
+ return nodes(calls).filter((node, index, all) => node !== all[index - 1]);
118
+ }
119
+
120
+ // ─── 1. The retriever ────────────────────────────────────────────────────────
121
+
122
+ describe("retrieval (shared.ts)", () => {
123
+ test("ranks the document a caller's words actually point at", () => {
124
+ expect(retrieve("my broadband light is red")[0]?.doc.id).toBe("D1");
125
+ expect(retrieve("nobody was home for the engineer")[0]?.doc.id).toBe("D6");
126
+ });
127
+
128
+ test("and gets the neighbouring document first often enough to need a grader", () => {
129
+ // "what does it cost to leave my contract early" ranks D7 (moving home,
130
+ // which mentions the contract AND the early exit fee) above D5, the
131
+ // document that actually answers it. Both come back, which is what makes
132
+ // the run recoverable — the document grader is the node that stops the
133
+ // neighbour being spoken as the answer, and this is the retrieval it is
134
+ // grading.
135
+ const ranked = retrieve("what does it cost to leave my contract early").map(
136
+ (one) => one.doc.id,
137
+ );
138
+ expect(ranked[0]).toBe("D7");
139
+ expect(ranked).toContain("D5");
140
+ });
141
+
142
+ test("returns nothing for a query with no content words", () => {
143
+ // Every term is a stopword, so there is nothing to rank — which the graph
144
+ // treats as a failed retrieval rather than as an empty knowledge base.
145
+ expect(retrieve("what about the one that you have")).toEqual([]);
146
+ });
147
+
148
+ test("a question phrased the way a caller speaks misses, which is the case the rewrite exists for", () => {
149
+ // The bait for the whole template: no document contains "dying", so lexical
150
+ // retrieval cannot reach D10 until `transform_query` supplies the words a
151
+ // document about it would use.
152
+ const spoken = retrieve("my internet keeps dying at night").map((one) => one.doc.id);
153
+ expect(spoken).not.toContain("D10");
154
+ expect(retrieve("evening slowdown congestion peak time")[0]?.doc.id).toBe("D10");
155
+ });
156
+ });
157
+
158
+ // ─── 2. The graph's routing ──────────────────────────────────────────────────
159
+
160
+ describe("corrective-RAG graph", () => {
161
+ test("the straight path grades, answers, and checks the answer twice", async () => {
162
+ const { generate, calls } = scriptedModel({
163
+ relevant: (id) => id === "D2",
164
+ answers: ["Power the hub off at the wall for thirty seconds."],
165
+ });
166
+ const trace = await runCorrectiveRag(generate, "how do I reboot the hub");
167
+
168
+ expect(phases(calls)).toEqual([
169
+ "grade_documents",
170
+ "generate",
171
+ "grade_generation_v_documents",
172
+ "grade_generation_v_question",
173
+ ]);
174
+ expect(trace.answer).toContain("thirty seconds");
175
+ expect(trace.grounded).toBe(true);
176
+ expect(trace.useful).toBe(true);
177
+ expect(trace.rewrites).toBe(0);
178
+ expect(trace.exhausted).toBe(false);
179
+ expect(trace.docs.filter((doc) => doc.relevant).map((doc) => doc.id)).toEqual(["D2"]);
180
+ });
181
+
182
+ test("nothing relevant rewrites the query and retries — the corrective edge", async () => {
183
+ const { generate, calls } = scriptedModel({
184
+ // First retrieval is graded useless; after the rewrite everything passes.
185
+ relevant: (_id, attempt) => attempt > 1,
186
+ rewrite: "evening slowdown congestion peak time",
187
+ answers: ["There is no traffic management, so evening slowdowns are local."],
188
+ });
189
+ const trace = await runCorrectiveRag(generate, "my internet keeps dying at night");
190
+
191
+ expect(calls.filter((call) => call === "transform_query")).toHaveLength(1);
192
+ expect(trace.rewrites).toBe(1);
193
+ expect(trace.query).toBe("evening slowdown congestion peak time");
194
+ expect(trace.answer).toContain("traffic management");
195
+ // The trace names the nodes the way their graph does, which is what the
196
+ // sidebar renders.
197
+ expect(trace.steps.map((entry) => entry.node)).toContain("transform_query");
198
+ });
199
+
200
+ test("a caller question nothing covers ends exhausted, with no answer", async () => {
201
+ const { generate, calls } = scriptedModel({ relevant: () => false });
202
+ const trace = await runCorrectiveRag(generate, "can I get a landline installed");
203
+
204
+ expect(trace.answer).toBeNull();
205
+ expect(trace.exhausted).toBe(true);
206
+ expect(trace.rewrites).toBe(MAX_ATTEMPTS - 1);
207
+ // It never reached the answer node: nothing was graded relevant to answer
208
+ // FROM, which is the whole point of grading before generating.
209
+ expect(nodes(calls)).not.toContain("generate");
210
+ });
211
+
212
+ test("an ungrounded answer is regenerated once and then withheld", async () => {
213
+ const { generate, calls } = scriptedModel({
214
+ relevant: (id) => id === "D5",
215
+ answers: ["The exit fee is nine months.", "The exit fee is still nine months."],
216
+ grounded: ["no", "no"],
217
+ });
218
+ const trace = await runCorrectiveRag(generate, "what is the early exit fee");
219
+
220
+ expect(calls.filter((call) => call === "generate")).toHaveLength(2);
221
+ // Withheld rather than spoken with a caveat: an answer the grader says is
222
+ // not in the documents is the one thing a support line must never say.
223
+ expect(trace.answer).toBeNull();
224
+ expect(trace.grounded).toBe(false);
225
+ expect(trace.exhausted).toBe(true);
226
+ expect(nodes(calls)).not.toContain("grade_generation_v_question");
227
+ });
228
+
229
+ test("a grounded but beside-the-point answer rewrites once, then is returned with its verdict", async () => {
230
+ const { generate } = scriptedModel({
231
+ relevant: () => true,
232
+ useful: ["no", "no"],
233
+ // The rewrite has to retrieve something, or the second attempt ends in
234
+ // the no-documents branch instead of the one under test.
235
+ rewrite: "direct debit bill payment date",
236
+ answers: ["Bills go out on the first.", "Bills go out on the first."],
237
+ });
238
+ const trace = await runCorrectiveRag(generate, "when will my money actually leave my account");
239
+
240
+ expect(trace.rewrites).toBe(1);
241
+ expect(trace.useful).toBe(false);
242
+ expect(trace.exhausted).toBe(true);
243
+ // Still returned: it is grounded, and the tool tells the model to offer a
244
+ // ticket alongside it rather than to withhold it.
245
+ expect(trace.answer).toBe("Bills go out on the first.");
246
+ });
247
+ });
248
+
249
+ // ─── 3. The tools ────────────────────────────────────────────────────────────
250
+
251
+ describe("answer_question", () => {
252
+ test("hands back the answer, its sources and its verdicts, and records the trace", async () => {
253
+ const { generate } = scriptedModel({
254
+ relevant: (id) => id === "D8",
255
+ answers: ["Area outages are on the status page, and rebooting will not help."],
256
+ });
257
+ const ctx = makeCtx(generate);
258
+ const result = (await run("answer_question", { question: "is there an outage" }, ctx)) as {
259
+ answer: string;
260
+ sources: string[];
261
+ grounded: boolean;
262
+ answersTheQuestion: boolean;
263
+ guidance?: string;
264
+ };
265
+
266
+ expect(result.answer).toContain("status page");
267
+ expect(result.sources).toEqual(["Checking for an outage in your area"]);
268
+ expect(result.grounded).toBe(true);
269
+ expect(result.answersTheQuestion).toBe(true);
270
+ expect(result.guidance).toBeUndefined();
271
+
272
+ const state = supportSlot.get(ctx);
273
+ expect(state.asked).toEqual(["is there an outage"]);
274
+ expect(state.trace?.answer).toBe(result.answer);
275
+ });
276
+
277
+ test("with nothing grounded it returns no answer and points at the ticket", async () => {
278
+ const { generate } = scriptedModel({ relevant: () => false });
279
+ const ctx = makeCtx(generate);
280
+ const result = (await run("answer_question", { question: "do you sell phones" }, ctx)) as {
281
+ answer: null;
282
+ guidance: string;
283
+ };
284
+ expect(result.answer).toBeNull();
285
+ expect(result.guidance).toContain("log_ticket");
286
+ });
287
+
288
+ test("a broken model call is reported rather than thrown at the turn", async () => {
289
+ // ctx.generate rejecting is the default `createToolContext` gives — a bad
290
+ // key in production looks the same from here.
291
+ const ctx = createToolContext({});
292
+ const result = (await run("answer_question", { question: "anything" }, ctx)) as {
293
+ error: string;
294
+ };
295
+ expect(result.error).toContain("knowledge base lookup failed");
296
+ });
297
+
298
+ test("two independent contexts never see each other's traces or tickets", async () => {
299
+ // What this really checks: the state lives in the SLOT and not in a
300
+ // module-level variable. `createToolContext()` hands each call its own
301
+ // detached slot store, so the isolation is per CONTEXT — two distinct
302
+ // session ids would prove nothing extra, and `sessionSlot` could stop
303
+ // keying by session with this still passing.
304
+ const { generate } = scriptedModel({ relevant: (id) => id === "D2", answers: ["Reboot it."] });
305
+ const first = makeCtx(generate);
306
+ const second = makeCtx(generate);
307
+
308
+ await run("answer_question", { question: "how do I reboot" }, first);
309
+ expect(supportSlot.get(second).trace).toBeNull();
310
+ expect(supportSlot.get(first).trace).not.toBeNull();
311
+ });
312
+ });
313
+
314
+ describe("log_ticket", () => {
315
+ test("logs a reference and keeps the callback number off the wire", async () => {
316
+ const ctx = createToolContext({});
317
+ const logged = (await run(
318
+ "log_ticket",
319
+ { question: "landline install", callback: "07700 900123" },
320
+ ctx,
321
+ )) as { reference: string };
322
+ expect(logged.reference).toBe("TCK4001");
323
+
324
+ const state = supportSlot.get(ctx);
325
+ expect(state.ticket?.callback).toBe("07700 900123");
326
+ // The projection is the privacy boundary — only the reference crosses.
327
+ const view = supportView(state);
328
+ expect(view.ticket).toBe("TCK4001");
329
+ expect(JSON.stringify(view)).not.toContain("900123");
330
+ });
331
+ });
332
+
333
+ describe("supportView projection", () => {
334
+ test("an untouched call projects an empty trace, not undefined", () => {
335
+ // Exactly the value client.tsx hoists as its fallback.
336
+ expect(supportSlot.projection(supportView)(undefined)).toMatchObject({
337
+ product: "Meridian Fibre",
338
+ trace: null,
339
+ asked: [],
340
+ ticket: null,
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,27 @@
1
+ import { agent } from "@alexkroman1/aai";
2
+ import { PRODUCT, supportSlot, supportView } from "./shared.ts";
3
+
4
+ /**
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`
7
+ * the attribution and the graders, `shared.ts` the knowledge base and why its
8
+ * retriever is lexical.
9
+ *
10
+ * **Why this shape is worth the model calls on a phone line.** A naive RAG voice
11
+ * agent retrieves, stuffs the documents into the prompt and speaks — and when
12
+ * retrieval missed, it answers from the nearest document it was handed, which
13
+ * over a phone is indistinguishable from a confident correct answer. The
14
+ * template's own knowledge base is built to bait exactly that: "cancelling your
15
+ * contract" and "cancelling an engineer visit" are two documents, two fees, and
16
+ * one word apart. The document grader is what makes the second one not get
17
+ * spoken as the answer to the first.
18
+ */
19
+ export default agent({
20
+ name: `${PRODUCT} Support`,
21
+ // The trace exists before the first tool call, so a resumed connection has
22
+ // something to project.
23
+ // The projection is also the privacy boundary: a logged ticket carries the
24
+ // caller's callback number, and only its reference crosses to the browser.
25
+ syncState: supportSlot.projection(supportView),
26
+ greeting: `${PRODUCT} support, you're through to the automated line. What's happened?`,
27
+ });
@@ -0,0 +1,122 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import { AutoScroll, client, useAgentState, useTheme } from "@alexkroman1/aai-ui";
3
+ import type { SupportView } from "./shared.ts";
4
+ import { PRODUCT, supportSlot, supportView } from "./shared.ts";
5
+
6
+ const EMPTY_SUPPORT: SupportView = supportSlot.projection(supportView)(undefined);
7
+
8
+ /**
9
+ * The graph, as it ran for the last question.
10
+ *
11
+ * This is the panel worth having: the whole argument for the corrective loop is
12
+ * work the caller never hears — four documents retrieved, three rejected, the
13
+ * question rewritten once — and a support line whose grading is invisible is a
14
+ * support line nobody can tell apart from one that just guesses well.
15
+ */
16
+ function TraceSidebar() {
17
+ const theme = useTheme();
18
+ const support = useAgentState<SupportView>(EMPTY_SUPPORT);
19
+ const trace = support.trace;
20
+
21
+ return (
22
+ <div className="flex h-full min-h-0 flex-col gap-4 p-4" style={{ color: theme.text }}>
23
+ <div>
24
+ <h3 className="text-sm font-bold uppercase tracking-wide opacity-60">{support.product}</h3>
25
+ <p className="text-xs opacity-50">
26
+ {support.asked.length} question{support.asked.length === 1 ? "" : "s"} this call
27
+ {support.ticket ? ` · ticket ${support.ticket}` : ""}
28
+ </p>
29
+ </div>
30
+
31
+ {!trace && (
32
+ <p className="text-sm opacity-50">
33
+ Ask a question and the retrieval, the grades and the verdicts appear here.
34
+ </p>
35
+ )}
36
+
37
+ {trace && (
38
+ <AutoScroll
39
+ scrollClassName="min-h-0 overflow-y-auto"
40
+ contentClassName="flex flex-col gap-4 pr-1"
41
+ >
42
+ <div className="rounded-lg p-3" style={{ background: theme.surface }}>
43
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Question</p>
44
+ <p className="text-sm">{trace.question}</p>
45
+ {trace.rewrites > 0 && (
46
+ <p className="mt-1 text-xs opacity-60">
47
+ rewritten to <span className="italic">{trace.query}</span>
48
+ </p>
49
+ )}
50
+ </div>
51
+
52
+ <div className="flex flex-col gap-1.5">
53
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
54
+ Retrieved &amp; graded
55
+ </p>
56
+ {trace.docs.length === 0 && <p className="text-xs opacity-50">Nothing retrieved.</p>}
57
+ {trace.docs.map((doc) => (
58
+ <div
59
+ key={doc.id}
60
+ className="flex items-start gap-2 rounded-lg p-2"
61
+ style={{ background: theme.surface, opacity: doc.relevant ? 1 : 0.5 }}
62
+ >
63
+ <span style={{ color: doc.relevant ? theme.primary : theme.text }}>
64
+ {doc.relevant ? "✓" : "×"}
65
+ </span>
66
+ <div className="min-w-0">
67
+ <p className="text-sm">{doc.title}</p>
68
+ <p className="text-xs opacity-60">{doc.reason}</p>
69
+ </div>
70
+ </div>
71
+ ))}
72
+ </div>
73
+
74
+ <div className="flex flex-col gap-1">
75
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Graph</p>
76
+ {trace.steps.map((entry, index) => (
77
+ // Steps are append-only within a run, so the index is stable.
78
+ <p key={`${index}-${entry.node}`} className="text-xs">
79
+ <span className="font-mono opacity-80">{entry.node}</span>
80
+ <span className="opacity-50"> — {entry.detail}</span>
81
+ </p>
82
+ ))}
83
+ </div>
84
+
85
+ <div className="rounded-lg p-3" style={{ background: theme.surface }}>
86
+ <p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Verdict</p>
87
+ {trace.answer ? (
88
+ <p className="text-sm">{trace.answer}</p>
89
+ ) : (
90
+ <p className="text-sm opacity-60">No answer could be grounded.</p>
91
+ )}
92
+ <p className="mt-2 text-xs opacity-60">
93
+ grounded: {String(trace.grounded)} · answers the question: {String(trace.useful)}
94
+ {trace.exhausted ? " · budget exhausted" : ""}
95
+ </p>
96
+ </div>
97
+ </AutoScroll>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ client({
104
+ // Derived, not typed twice: `PRODUCT` comes off `knowledge.json`, which is
105
+ // what `agent.ts` names the agent and greets with. A knowledge base swapped
106
+ // for another product otherwise leaves the browser tab advertising the old
107
+ // one.
108
+ name: `${PRODUCT} Support`,
109
+ sidebar: TraceSidebar,
110
+ theme: {
111
+ bg: "#0b1220",
112
+ primary: "#4ea8de",
113
+ text: "#e8eef6",
114
+ surface: "#141d2e",
115
+ border: "#1f2b3f",
116
+ },
117
+ tools: {
118
+ answer_question: { icon: "\u{1F50D}", label: "Checking the knowledge base" },
119
+ list_topics: { icon: "\u{1F4DA}", label: "Listing topics" },
120
+ log_ticket: { icon: "\u{1F4DD}", label: "Logging a ticket" },
121
+ },
122
+ });