@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,223 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * Specs for the link digest — the workflow-app template.
4
+ *
5
+ * There are no tools to exercise, which is the point: what this template
6
+ * demonstrates is a DECLARATION, so the spec asserts the declaration. Three
7
+ * things carry the shape and each is silent when wrong — the `page: "static"`
8
+ * field (without it the deployed agent still offers a `/websocket` nothing will
9
+ * answer), the workflow's NAME (the page starts a run by that string, so a
10
+ * rename here is a runtime 400 there), and the input schema (which is both the
11
+ * call-site validation and the JSON Schema `GET /workflows` serves).
12
+ *
13
+ * The workflow BODY is not tested here: it is only durable once the Workflow
14
+ * DevKit's build has transformed it, so a unit test of it would exercise a plain
15
+ * async function and prove nothing about replay. Its STEPS are, and directly —
16
+ * imported with no bundler in the path a `"use step"` function is an ordinary
17
+ * async function, so its HTML handling, its JSON contract with the model and its
18
+ * `FatalError` guards are all testable.
19
+ */
20
+
21
+ import { stubStepFetch } from "@alexkroman1/aai/testing";
22
+ import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
23
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
24
+ import agentDef, { digest } from "./agent.ts";
25
+ import { extractText, extractTitle, fetchArticle, summarize } from "./workflows/digest.ts";
26
+
27
+ describe("the agent declares itself a workflow app", () => {
28
+ test("its front door is a page, not a microphone", () => {
29
+ // Not decoration: `createServer` declines `/websocket` with a reason for a
30
+ // static agent, and telephony defaults off.
31
+ expect(agentDef.page).toBe("static");
32
+ });
33
+
34
+ test("it declares no voice pipeline and no tools, because nothing talks", () => {
35
+ expect(agentDef.stt).toBeUndefined();
36
+ expect(agentDef.llm).toBeUndefined();
37
+ expect(agentDef.tts).toBeUndefined();
38
+ expect(agentDef.s2s).toBeUndefined();
39
+ expect(agentDef.tools).toEqual({});
40
+ });
41
+
42
+ test("under the name the page starts a run by", () => {
43
+ // `api.start("digest", …)` in client.tsx names this key. Nothing else
44
+ // records it, so a rename here is a 400 there rather than a compile error.
45
+ expect(Object.keys(agentDef.workflows ?? {})).toEqual(["digest"]);
46
+ expect(agentDef.workflows?.digest).toBe(digest);
47
+ });
48
+ });
49
+
50
+ describe("the input schema", () => {
51
+ test("accepts a URL", async () => {
52
+ const result = await digest.input?.["~standard"].validate({ url: "https://example.com/a" });
53
+ expect(result?.issues).toBeUndefined();
54
+ });
55
+
56
+ test("rejects a non-URL at the CALL SITE rather than three steps into a run", async () => {
57
+ const result = await digest.input?.["~standard"].validate({ url: "not a url" });
58
+ expect(result?.issues).toBeDefined();
59
+ });
60
+
61
+ test("carries a description, which is what a rendered form labels the field with", () => {
62
+ expect(digest.description).toBeTruthy();
63
+ expect(digest.input).toBeDefined();
64
+ });
65
+ });
66
+
67
+ describe("the agent declares the credential its steps read", () => {
68
+ test("so a deploy checks for it rather than the first run", () => {
69
+ // A workflow app declares no providers, so `requiredEnv` is the ONLY thing
70
+ // in its config that can name a credential.
71
+ expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
72
+ });
73
+ });
74
+
75
+ describe("extractText", () => {
76
+ test("drops script and style CONTENT, not just their tags", () => {
77
+ // Stripping tags alone leaves a page's JavaScript in the prompt — expensive,
78
+ // and a way to smuggle instructions past the reader.
79
+ const text = extractText(
80
+ "<html><head><style>body{color:red}</style></head><body><script>alert('x')</script><p>Real words here.</p></body></html>",
81
+ );
82
+ expect(text).toBe("Real words here.");
83
+ });
84
+
85
+ test("decodes &amp; LAST, so an escaped entity is not decoded twice", () => {
86
+ // `&amp;lt;` is a literal `&lt;` on the page; decoding `&amp;` first would
87
+ // turn it into a `<` the author never wrote.
88
+ expect(extractText("<p>a &amp;lt; b &amp; c</p>")).toBe("a &lt; b & c");
89
+ });
90
+
91
+ test("collapses whitespace, because HTML indentation is not prose", () => {
92
+ expect(extractText("<p>one</p>\n\n <p>two</p>")).toBe("one two");
93
+ });
94
+
95
+ test("caps what crosses the queue to the next step", () => {
96
+ const huge = `<p>${"word ".repeat(20_000)}</p>`;
97
+ expect(extractText(huge).length).toBeLessThanOrEqual(24_000);
98
+ });
99
+ });
100
+
101
+ describe("extractTitle", () => {
102
+ test("reads the document title", () => {
103
+ expect(extractTitle("<html><title> Otters &amp; tools </title></html>")).toBe(
104
+ "Otters & tools",
105
+ );
106
+ });
107
+
108
+ test("answers undefined when there is none, so the caller can fall back", () => {
109
+ expect(extractTitle("<html><body>hi</body></html>")).toBeUndefined();
110
+ });
111
+ });
112
+
113
+ describe("fetchArticle", () => {
114
+ /** Unpublished between specs — a fetch left behind reaches the next file. */
115
+ let restore: (() => void) | undefined;
116
+ afterEach(() => {
117
+ restore?.();
118
+ restore = undefined;
119
+ });
120
+
121
+ /**
122
+ * A page server answering `html` with `status`.
123
+ *
124
+ * Published into `stepFetch`'s own slot rather than over `globalThis.fetch`:
125
+ * the step calls `stepFetch`, and stubbing the global would pass while
126
+ * exercising the fallback path production never takes.
127
+ */
128
+ function stubPage(html: string, status = 200) {
129
+ const stub = stubStepFetch(() => ({
130
+ status,
131
+ body: html,
132
+ headers: { "Content-Type": "text/html" },
133
+ }));
134
+ restore = stub.restore;
135
+ return stub;
136
+ }
137
+
138
+ test("returns the page's title and its readable text", async () => {
139
+ stubPage(
140
+ `<html><title>Otters</title><body><p>${"Otters use tools. ".repeat(20)}</p></body></html>`,
141
+ );
142
+ const article = await fetchArticle("https://example.com/otters");
143
+ expect(article.title).toBe("Otters");
144
+ expect(article.text).toContain("Otters use tools.");
145
+ expect(article.url).toBe("https://example.com/otters");
146
+ });
147
+
148
+ test("falls back to the hostname when the page has no title", async () => {
149
+ stubPage(`<html><body><p>${"Otters use tools. ".repeat(20)}</p></body></html>`);
150
+ expect((await fetchArticle("https://example.com/otters")).title).toBe("example.com");
151
+ });
152
+
153
+ test("fails FATALLY on a page with no readable text", async () => {
154
+ // A JS-rendered site is the usual cause, and no number of attempts fixes it.
155
+ stubPage("<html><body><div id='root'></div></body></html>");
156
+ await expect(fetchArticle("https://example.com/app")).rejects.toThrow(/no readable text/);
157
+ });
158
+
159
+ test("fails FATALLY on a 404 and plainly on a 503", async () => {
160
+ stubPage("", 404);
161
+ await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 404/);
162
+ stubPage("", 503);
163
+ await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 503/);
164
+ });
165
+ });
166
+
167
+ describe("summarize", () => {
168
+ const ARTICLE = { url: "https://example.com/a", title: "Otters", text: "Otters use tools." };
169
+
170
+ beforeEach(() => {
171
+ // `stepEnv` falls back to the process env when no host has published one,
172
+ // which is exactly the case a spec is. `unstubEnvs` clears it per test.
173
+ vi.stubEnv("ASSEMBLYAI_API_KEY", "sk-test");
174
+ });
175
+
176
+ test("returns the headline and points the model produced", async () => {
177
+ const calls = stubGateway('{"headline":"Otters are clever","points":["a","b","c"]}');
178
+ const result = await summarize(ARTICLE);
179
+
180
+ expect(result).toEqual({
181
+ url: ARTICLE.url,
182
+ headline: "Otters are clever",
183
+ points: ["a", "b", "c"],
184
+ });
185
+ // The key is a BEARER here — the gateway is OpenAI-compatible, unlike
186
+ // AssemblyAI's streaming sockets, which take it raw.
187
+ expect(calls[0]?.headers.authorization).toBe("Bearer sk-test");
188
+ });
189
+
190
+ test("unwraps a fenced reply rather than failing on it", async () => {
191
+ stubGateway('```json\n{"headline":"H","points":["a"]}\n```');
192
+ expect((await summarize(ARTICLE)).headline).toBe("H");
193
+ });
194
+
195
+ test("throws PLAINLY when the model answered with prose, so the step retries", async () => {
196
+ // The distinction that is the whole retry policy: a model that ignored the
197
+ // format may well obey on the next attempt, where a 401 will not. Plain
198
+ // means NOT a `FatalError`, which is what the DevKit stops retrying on.
199
+ stubGateway("Here is a summary of the article about otters.");
200
+ const err = await summarize(ARTICLE).catch((thrown: unknown) => thrown);
201
+ expect(err).toBeInstanceOf(Error);
202
+ expect((err as Error).name).not.toBe("FatalError");
203
+ expect(err).toMatchObject({ message: expect.stringContaining("Expected JSON from the model") });
204
+ });
205
+
206
+ test("rejects JSON of the wrong shape as firmly as no JSON at all", async () => {
207
+ // The reply parses and is an object, so only the SCHEMA catches it — which
208
+ // is what taking a schema bought over the guard this used to hand-roll: the
209
+ // failure NAMES the field that was missing.
210
+ stubGateway('{"headline":"H"}');
211
+ await expect(summarize(ARTICLE)).rejects.toThrow(/did not match the shape: points/);
212
+ });
213
+
214
+ test("fails FATALLY with no API key rather than retrying five times", async () => {
215
+ vi.stubEnv("ASSEMBLYAI_API_KEY", "");
216
+ stubGateway('{"headline":"H","points":["a"]}');
217
+ await expect(summarize(ARTICLE)).rejects.toThrow(/ASSEMBLYAI_API_KEY/);
218
+ });
219
+
220
+ test("retries beyond the default, because a rate limit and a bad format both happen", () => {
221
+ expect(summarize.maxRetries).toBeGreaterThan(3);
222
+ });
223
+ });
@@ -0,0 +1,79 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * A WORKFLOW APP — the worked example for `workflowApp()`.
4
+ *
5
+ * Its front door is a form, not a microphone. There is no session, no
6
+ * WebSocket, and no voice pipeline: the page (`client.tsx`) starts a run over
7
+ * the workflow HTTP API and watches it, and the run outlives the tab.
8
+ *
9
+ * ## What makes this a different KIND of agent from `research-workflow`
10
+ *
11
+ * `research-workflow` is a voice agent that HANDS OFF to a workflow — a caller is on
12
+ * the line, so a tool starts a run and answers the turn. Here the workflow is
13
+ * the entire product, and the declaration says so:
14
+ *
15
+ * - `workflowApp()` is `agent({ …, page: "static" })` with the discriminant
16
+ * already set. The declaration is not decoration: `createServer` declines
17
+ * `/websocket` with a reason (so a page mounted with `client()` by mistake
18
+ * fails the same way in `aai dev` and in production rather than only after a
19
+ * deploy) and telephony defaults off.
20
+ * - There is no `stt`/`llm`/`tts`, no `tools` and no `systemPrompt` — and they
21
+ * are not merely omitted, they are UNDECLARABLE here. Nothing talks and no
22
+ * model runs, so every one of them was inert; this file used to carry a
23
+ * `systemPrompt` addressed to a model that never ran.
24
+ *
25
+ * ## What the page needs from this file, and how it gets it
26
+ *
27
+ * Only the workflow's NAME and its output TYPE. The name is the key in
28
+ * `workflows` below — `"digest"` — and the type is
29
+ * `WorkflowOutputOf<typeof digest>`, derived from a `import type` of this module
30
+ * which is ERASED at build time, so naming it pulls no server graph into the
31
+ * browser bundle. Nothing is generated and nothing is restated.
32
+ *
33
+ * ## What it needs
34
+ *
35
+ * `ASSEMBLYAI_API_KEY` in the agent env — `.env` under `aai dev`, `aai secret
36
+ * put ASSEMBLYAI_API_KEY` once deployed — because the run really reads the page
37
+ * and really summarizes it with a model. `requiredEnv` below is what makes a
38
+ * deploy check for it rather than letting the first run find out, and it is
39
+ * load-bearing here in a way it is not for a voice agent: a workflow app
40
+ * declares no providers, so nothing else in its config names a credential.
41
+ *
42
+ * A step is handed no `ToolContext`, so it reads that key with `requireStepEnv`
43
+ * rather than `ctx.env`; see `workflows/digest.ts` and `research-workflow`'s module
44
+ * doc for the one thing that changes under `aai dev` (the key has to be in
45
+ * `.env`, not just your shell).
46
+ *
47
+ * Requires storage (`aai storage enable`, or `DATABASE_URL` under `aai dev`) —
48
+ * runs and the correlation-key index both live there.
49
+ */
50
+
51
+ import { workflow, workflowApp } from "@alexkroman1/aai";
52
+ import { z } from "zod";
53
+ import { digestFlow } from "./workflows/digest.ts";
54
+
55
+ /**
56
+ * The declaration: schema, description, and the directive body.
57
+ *
58
+ * The `input` schema does double duty here in a way it cannot for a voice agent.
59
+ * It validates at `start()` — so a bad URL is a 400 at the call site rather than
60
+ * a failed run discovered later — and it is served on `GET /workflows` as JSON
61
+ * Schema, which is what lets a page render a form from a workflow it was not
62
+ * written against.
63
+ */
64
+ export const digest = workflow({
65
+ description: "Read a link, reduce it to a headline and three points, then file the digest",
66
+ input: z.object({
67
+ url: z.url().describe("The link to digest"),
68
+ }),
69
+ run: digestFlow,
70
+ });
71
+
72
+ export default workflowApp({
73
+ name: "Link Digest",
74
+ // The whole product. A workflow app is an agent whose work happens here.
75
+ workflows: { digest },
76
+ // Checked at deploy time. A workflow app declares no providers, so this is the
77
+ // only thing that can name the credential its steps read.
78
+ requiredEnv: ["ASSEMBLYAI_API_KEY"],
79
+ });
@@ -0,0 +1,146 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * The browser half of a workflow app.
4
+ *
5
+ * Mounted with `page()` rather than `client()`: there is no session to build, so
6
+ * there is no socket, no audio graph, and no microphone request. Everything else
7
+ * is the same — the same `client.tsx` filename, React, Tailwind, and the same
8
+ * theme tokens the voice components read.
9
+ *
10
+ * What replaces `useSession()` is three things: `createWorkflowApi()` to start a
11
+ * run, `useWorkflowRun()` to watch its STATUS, and `useWorkflowProgress()` to
12
+ * read what it has WRITTEN. The API is durable, so the `runId` is the whole
13
+ * state — it survives a reload, a different device, or `curl`.
14
+ *
15
+ * ## Status and progress are different questions
16
+ *
17
+ * `useWorkflowRun` answers "where has this got to" from the world's own record —
18
+ * pending, running, completed. `useWorkflowProgress` answers "what is it doing"
19
+ * from what the run wrote itself (`report()` in `workflows/digest.ts`). A page
20
+ * with only the first shows "Working…" for the length of the run; a page with
21
+ * only the second cannot tell a finished run from a quiet one. Both are cheap:
22
+ * one stream each, ended by the agent when there is nothing left to say.
23
+ *
24
+ * Progress also REPLAYS — chunks are retained with the run — so a reload mid-run
25
+ * catches up rather than starting from whatever arrives next. This page renders
26
+ * only the newest line, because on a page this small that is the whole of what a
27
+ * status wants; `transcription-workflow` renders the full log, where a fan-out makes
28
+ * the history worth seeing.
29
+ */
30
+
31
+ import { createWorkflowApi, page, useWorkflowProgress, useWorkflowRun } from "@alexkroman1/aai-ui";
32
+ import "@alexkroman1/aai-ui/styles.css";
33
+ // ERASED at build time, so naming the agent's own type costs the browser bundle
34
+ // nothing — and it is what stops this file restating a shape `workflows/
35
+ // digest.ts` already declares.
36
+ import type { WorkflowOutputOf } from "@alexkroman1/aai";
37
+ // The one runtime import from the SDK a browser bundle wants: `/utils` is the
38
+ // zod-free subpath, so it costs a few hundred bytes rather than the root
39
+ // barrel's module graph.
40
+ import { errorMessage } from "@alexkroman1/aai/utils";
41
+ import { useState } from "react";
42
+ import type { digest } from "./agent.ts";
43
+
44
+ /** What a completed run reports, derived from the workflow rather than restated. */
45
+ type Digest = WorkflowOutputOf<typeof digest>;
46
+
47
+ /**
48
+ * Hoisted out of the component deliberately.
49
+ *
50
+ * `useWorkflowRun` holds the client in a ref precisely so a fresh object per
51
+ * render cannot restart its watch, but building one in render is still a new
52
+ * `fetch` closure every time and reads as though it were free.
53
+ */
54
+ const api = createWorkflowApi();
55
+
56
+ export function App() {
57
+ const [url, setUrl] = useState("");
58
+ const [runId, setRunId] = useState<string>();
59
+ const [error, setError] = useState<string>();
60
+ // The generic is what makes `run.status === "completed"` narrow to a TYPED
61
+ // `run.output` instead of `unknown`.
62
+ const { run, polling } = useWorkflowRun<Digest>(runId, { api });
63
+ // What the run has SAID, as against where it has got to. Defaults to `string`,
64
+ // which is what `report()` writes.
65
+ const { latest, supported } = useWorkflowProgress(runId, { api });
66
+
67
+ const submit = async (event: React.FormEvent) => {
68
+ event.preventDefault();
69
+ setError(undefined);
70
+ try {
71
+ // Resolves as soon as the run exists — deliberately not when it finishes.
72
+ // That is the whole mechanism: the digest sleeps for a while, and this
73
+ // page is free to be closed in the meantime.
74
+ setRunId(await api.start("digest", { url }));
75
+ } catch (err) {
76
+ // The agent's own sentence: an input failing the workflow's schema names
77
+ // the issue, which is better copy than anything this page could write.
78
+ setError(errorMessage(err));
79
+ }
80
+ };
81
+
82
+ return (
83
+ <main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
84
+ <h1 className="text-2xl font-medium">Link Digest</h1>
85
+
86
+ <form onSubmit={submit} className="flex gap-2">
87
+ <input
88
+ type="url"
89
+ required
90
+ value={url}
91
+ onChange={(e) => setUrl(e.target.value)}
92
+ placeholder="https://example.com/article"
93
+ className="flex-1 rounded-md border px-3 py-2"
94
+ />
95
+ <button type="submit" disabled={polling} className="rounded-md border px-4 py-2">
96
+ {polling ? "Working…" : "Digest"}
97
+ </button>
98
+ </form>
99
+
100
+ {error !== undefined && <p className="text-red-600">{error}</p>}
101
+
102
+ {/* A run that has not settled says so. `polling` is not derivable from the
103
+ snapshot alone — an id the agent never knew leaves `run` undefined,
104
+ which would otherwise read as "still waiting" forever. */}
105
+ {polling && <p>You can close this tab — the run continues without it.</p>}
106
+
107
+ {/* The run's own narration, newest line only — see the module doc.
108
+
109
+ `supported` is what keeps this from being blank forever on an agent
110
+ deployed before progress streams existed: "wrote nothing yet" and
111
+ "serves no stream" are indistinguishable from `progress` alone. */}
112
+ {supported && latest !== undefined && <p className="text-sm opacity-70">{latest}</p>}
113
+
114
+ {/* The counterpart of the `sleep` in `workflows/digest.ts`. Without it the
115
+ only handle on a sleeping run is `cancel`, so "file it now" and "throw
116
+ it away" would be the same button. `wake` answering 0 means the run had
117
+ already moved past its wait, which is why nothing here treats that as a
118
+ failure. */}
119
+ {runId !== undefined && polling && (
120
+ <button
121
+ type="button"
122
+ onClick={() => void api.wake(runId)}
123
+ className="self-start rounded-md border px-3 py-1 text-sm"
124
+ >
125
+ File it now
126
+ </button>
127
+ )}
128
+
129
+ {run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
130
+
131
+ {run?.status === "completed" && (
132
+ <article className="flex flex-col gap-3">
133
+ <h2 className="text-xl">{run.output.headline}</h2>
134
+ <ul className="flex list-disc flex-col gap-1 pl-5">
135
+ {run.output.points.map((point) => (
136
+ <li key={point}>{point}</li>
137
+ ))}
138
+ </ul>
139
+ <p className="text-sm opacity-70">Filed {run.output.filedAt}</p>
140
+ </article>
141
+ )}
142
+ </main>
143
+ );
144
+ }
145
+
146
+ page({ name: "Link Digest", component: App });