@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,222 @@
1
+ /**
2
+ * Specs for the redline desk — the reflection port, as a workflow app.
3
+ *
4
+ * Same honest line as `link-digest`'s spec, for the same reason: the workflow
5
+ * BODY is only durable once the Workflow DevKit's build has transformed it, so
6
+ * testing it here would exercise a plain async function and prove nothing about
7
+ * replay. What IS testable is the declaration (three things that are silent when
8
+ * wrong — the `page: "static"` field, the workflow's name, and the input schema)
9
+ * and the STEPS, which imported with no bundler in the path are ordinary async
10
+ * functions: their JSON contract with the model, their `FatalError` guards, and
11
+ * the pure helpers underneath them.
12
+ *
13
+ * The loop's EXIT is the one thing worth naming that a spec here cannot reach.
14
+ * It is decided in the body, on a step's journaled verdict — see the module doc
15
+ * in `workflows/redline.ts` — and what would prove it is a replay, which needs a
16
+ * built world. The critique step's verdict handling is where the testable half
17
+ * of that lives.
18
+ */
19
+
20
+ import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
21
+ import { beforeEach, describe, expect, test, vi } from "vitest";
22
+ import { FatalError } from "workflow";
23
+ import agentDef, { MAX_ROUNDS, redline } from "./agent.ts";
24
+ import {
25
+ briefBlock,
26
+ clampScore,
27
+ countWords,
28
+ critiqueDraft,
29
+ MAX_NOTES,
30
+ type RedlineInput,
31
+ reviseDraft,
32
+ writeDraft,
33
+ } from "./workflows/redline.ts";
34
+
35
+ const INPUT: RedlineInput = {
36
+ brief: "Explain why our API returns 402 when a workspace is over budget.",
37
+ audience: "customers",
38
+ rounds: 2,
39
+ mustCover: ["what to do about it", "how to raise the cap"],
40
+ };
41
+
42
+ // ─── 1. The declaration ──────────────────────────────────────────────────────
43
+
44
+ describe("the agent declares itself a workflow app", () => {
45
+ test("its front door is a page, not a microphone", () => {
46
+ // The port's own decision: seven long-form model calls in sequence is not
47
+ // something anyone holds a phone for.
48
+ expect(agentDef.page).toBe("static");
49
+ });
50
+
51
+ test("it declares no voice pipeline and no tools, because nothing talks", () => {
52
+ expect(agentDef.stt).toBeUndefined();
53
+ expect(agentDef.llm).toBeUndefined();
54
+ expect(agentDef.tts).toBeUndefined();
55
+ expect(agentDef.tools).toEqual({});
56
+ });
57
+
58
+ test("under the name the page starts a run by", () => {
59
+ // `useWorkflowSubmit("redline")` in client.tsx names this key. Nothing else
60
+ // records it, so a rename here is a 400 there rather than a compile error.
61
+ expect(Object.keys(agentDef.workflows ?? {})).toEqual(["redline"]);
62
+ expect(agentDef.workflows?.redline).toBe(redline);
63
+ });
64
+
65
+ test("and declares the credential its steps read", () => {
66
+ // A workflow app declares no providers, so `requiredEnv` is the ONLY thing
67
+ // in its config that can name a credential.
68
+ expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
69
+ });
70
+ });
71
+
72
+ describe("the input schema", () => {
73
+ const validate = (value: unknown) => redline.input?.["~standard"].validate(value);
74
+
75
+ test("caps the rounds at the CALL SITE rather than on the bill", async () => {
76
+ const tooMany = await validate({ ...INPUT, rounds: MAX_ROUNDS + 1 });
77
+ expect(tooMany?.issues).toBeDefined();
78
+ });
79
+
80
+ test("defaults the rounds and the required points, so the form need not", async () => {
81
+ const result = await validate({ brief: INPUT.brief, audience: "engineers" });
82
+ if (!result || result.issues) throw new Error("expected valid input");
83
+ expect(result.value).toMatchObject({ rounds: 2, mustCover: [] });
84
+ });
85
+
86
+ test("rejects an audience outside the enum — which is also what makes it a select", async () => {
87
+ // `<WorkflowFields>` renders a `z.enum` as a `<SelectField>`; the same
88
+ // declaration is what stops an API caller inventing a fifth audience.
89
+ expect((await validate({ ...INPUT, audience: "cats" }))?.issues).toBeDefined();
90
+ });
91
+
92
+ test("declares mustCover as an array, which is what the page renders by hand", async () => {
93
+ // The mixed-form case: `<WorkflowFields>` renders scalars only, so client.tsx
94
+ // writes this field itself and maps a textarea into it.
95
+ expect((await validate({ ...INPUT, mustCover: "one point" }))?.issues).toBeDefined();
96
+ expect((await validate({ ...INPUT, mustCover: ["one point"] }))?.issues).toBeUndefined();
97
+ });
98
+ });
99
+
100
+ // ─── 2. Pure helpers ─────────────────────────────────────────────────────────
101
+
102
+ describe("pure helpers", () => {
103
+ test("briefBlock restates the brief the same way for all three stages", () => {
104
+ const block = briefBlock(INPUT);
105
+ expect(block).toContain(INPUT.brief);
106
+ expect(block).toContain("customers");
107
+ expect(block).toContain("- how to raise the cap");
108
+ });
109
+
110
+ test("briefBlock says so when nothing was required, rather than showing an empty list", () => {
111
+ expect(briefBlock({ ...INPUT, mustCover: [] })).toContain("nothing specific");
112
+ });
113
+
114
+ test("countWords ignores surrounding and repeated whitespace", () => {
115
+ expect(countWords(" one two\nthree ")).toBe(3);
116
+ expect(countWords(" ")).toBe(0);
117
+ });
118
+
119
+ test("clampScore holds a model's number inside the range it was given", () => {
120
+ expect(clampScore(12)).toBe(10);
121
+ expect(clampScore(0)).toBe(1);
122
+ expect(clampScore(7.4)).toBe(7);
123
+ expect(clampScore(Number.NaN)).toBe(0);
124
+ });
125
+ });
126
+
127
+ // ─── 3. The steps ────────────────────────────────────────────────────────────
128
+
129
+ describe("the steps", () => {
130
+ beforeEach(() => {
131
+ // `stepEnv` falls back to the process env when no host has published one,
132
+ // which is exactly the case a spec is. `unstubEnvs` clears it per test.
133
+ vi.stubEnv("ASSEMBLYAI_API_KEY", "sk-test");
134
+ });
135
+
136
+ describe("writeDraft", () => {
137
+ test("returns the piece the model wrote, trimmed", async () => {
138
+ const calls = stubGateway("\n A draft about 402s. \n");
139
+ expect(await writeDraft(INPUT)).toBe("A draft about 402s.");
140
+ // The brief reaches the writer — otherwise it writes something else well.
141
+ expect(calls[0]?.prompt).toContain("how to raise the cap");
142
+ });
143
+
144
+ test("fails FATALLY on a brief that is only whitespace", async () => {
145
+ // The schema's `.min(20)` counts CHARACTERS, so twenty spaces validate at
146
+ // `start()` and arrive here as nothing to write from. No number of
147
+ // attempts makes a brief longer, so it is fatal rather than retryable.
148
+ stubGateway("unused");
149
+ await expect(writeDraft({ ...INPUT, brief: " ".repeat(25) })).rejects.toThrow(/too short/);
150
+ });
151
+
152
+ test("lets the SDK refuse an empty completion, RETRYABLY", async () => {
153
+ // Neither writer step carries an empty-reply guard, because `stepGenerate`
154
+ // already has one — and its verdict is the one that matters: retryable, so
155
+ // a blank answer costs an attempt rather than the run. A hand-written
156
+ // check here would have to re-derive that and could get it wrong.
157
+ stubGateway(" ");
158
+ const failure = await writeDraft(INPUT).catch((err: unknown) => err);
159
+ expect(failure).toBeInstanceOf(Error);
160
+ expect(failure).not.toBeInstanceOf(FatalError);
161
+ expect(String(failure)).toMatch(/empty completion/);
162
+ });
163
+ });
164
+
165
+ describe("critiqueDraft", () => {
166
+ test("returns the verdict the body branches on, with the score clamped", async () => {
167
+ stubGateway('{"verdict":"ship","score":42,"notes":["nothing major"]}');
168
+ const critique = await critiqueDraft("A draft.", INPUT, 1);
169
+ expect(critique.verdict).toBe("ship");
170
+ expect(critique.score).toBe(10);
171
+ });
172
+
173
+ test("keeps at most the notes the prompt asked for", async () => {
174
+ const notes = JSON.stringify(["a", "b", "c", "d", "e"]);
175
+ stubGateway(`{"verdict":"revise","score":6,"notes":${notes}}`);
176
+ const critique = await critiqueDraft("A draft.", INPUT, 1);
177
+ expect(critique.notes).toHaveLength(MAX_NOTES);
178
+ });
179
+
180
+ test("unwraps a fenced reply rather than failing on it", async () => {
181
+ stubGateway('```json\n{"verdict":"revise","score":5,"notes":["thin"]}\n```');
182
+ expect((await critiqueDraft("A draft.", INPUT, 1)).notes).toEqual(["thin"]);
183
+ });
184
+
185
+ test("throws PLAINLY when the critic answered with prose, so the step retries", async () => {
186
+ // The distinction that is the whole retry policy: a model that ignored the
187
+ // format may well obey on the next attempt, where a 401 will not. Plain
188
+ // means NOT a `FatalError`, which is what the DevKit stops retrying on.
189
+ stubGateway("I think the draft is pretty good, honestly.");
190
+ const err = await critiqueDraft("A draft.", INPUT, 1).catch((thrown: unknown) => thrown);
191
+
192
+ expect(FatalError.is(err)).toBe(false);
193
+ expect((err as Error).message).toMatch(/Expected JSON from the model/);
194
+ });
195
+
196
+ test("rejects a verdict outside the two the loop understands", async () => {
197
+ // Anything else would be read as "not ship" and quietly cost a round. The
198
+ // schema NAMES the field, which the hand-written guard this replaced could
199
+ // not: it answered a bare false.
200
+ stubGateway('{"verdict":"looks fine","score":8,"notes":[]}');
201
+ await expect(critiqueDraft("A draft.", INPUT, 1)).rejects.toThrow(
202
+ /did not match the shape: verdict/,
203
+ );
204
+ });
205
+ });
206
+
207
+ describe("reviseDraft", () => {
208
+ const CRITIQUE = { verdict: "revise" as const, score: 5, notes: ["Say what to do about it."] };
209
+
210
+ test("sends the notes to the reviser and returns the revision", async () => {
211
+ const calls = stubGateway("A better draft about 402s.");
212
+ const revised = await reviseDraft("A draft.", CRITIQUE, INPUT, 1);
213
+ expect(revised).toBe("A better draft about 402s.");
214
+ expect(calls[0]?.prompt).toContain("Say what to do about it.");
215
+ });
216
+
217
+ test("trims what the model returned, since it goes on to be the output", async () => {
218
+ stubGateway("\n A better draft. \n");
219
+ expect(await reviseDraft("A draft.", CRITIQUE, INPUT, 1)).toBe("A better draft.");
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A WORKFLOW APP — LangGraph's reflection agent, as a thing you submit work to.
3
+ *
4
+ * `link-digest` owns the shape and is the one to read first (`workflowApp()`, no
5
+ * session, no tools, a form that starts a run and a page that watches it), and
6
+ * none of that is restated here. What this one is FOR is the mechanism in
7
+ * `workflows/redline.ts`: write a piece, grade it against its brief, revise, and
8
+ * go round again until the critic says ship or the rounds run out.
9
+ *
10
+ * ## Why this one is not a voice agent
11
+ *
12
+ * It was the first question worth answering, and the answer is arithmetic: a
13
+ * three-round redline is up to seven long-form model calls in sequence. Nobody
14
+ * holds a phone for that, and nothing useful can be said down the line while it
15
+ * happens — the interesting output is a piece of prose to READ, not two
16
+ * sentences to hear. The same test sorts the other ports in this repo:
17
+ * `travel-concierge`, `support-line` and `plan-and-execute` all answer a caller inside a
18
+ * turn, so they are voice agents; this one and `transcription-workflow` produce a
19
+ * document, so they are pages over durable runs.
20
+ *
21
+ * ## What it needs
22
+ *
23
+ * - **`ASSEMBLYAI_API_KEY` in the agent env** — `.env` under `aai dev`,
24
+ * `aai secret put ASSEMBLYAI_API_KEY` once deployed. A step is handed no
25
+ * `ToolContext`, so it reads that key with `requireStepEnv` (inside
26
+ * `stepGenerate`); `requiredEnv` below is what makes a deploy check for it
27
+ * rather than letting the first run find out.
28
+ * - **Storage** (`aai storage enable`, or `DATABASE_URL` under `aai dev`) — runs
29
+ * live there.
30
+ */
31
+
32
+ import { workflow, workflowApp } from "@alexkroman1/aai";
33
+ import { z } from "zod";
34
+ import { redlineFlow } from "./workflows/redline.ts";
35
+
36
+ /** The most rounds one run may spend. Their `should_continue` cap, as an input:
37
+ * the critic can stop earlier, and nothing can go past this. */
38
+ export const MAX_ROUNDS = 3;
39
+
40
+ /**
41
+ * The declaration: schema, description, and the directive body.
42
+ *
43
+ * The input schema is doing three jobs at once here, which is the thing to
44
+ * notice. It validates at `start()`, so a `rounds: 40` is a 400 at the call site
45
+ * rather than forty model calls discovered on the bill; it is served on
46
+ * `GET /workflows` as JSON Schema, which is what lets `<WorkflowFields>` render
47
+ * most of this form without the page naming a field; and it is the type the
48
+ * body reads.
49
+ *
50
+ * `mustCover` is an ARRAY, deliberately. `<WorkflowFields>` renders scalars only
51
+ * — there is no honest control for an array — so the page writes that one field
52
+ * by hand in the same `<Form>` and maps it on submit. That mixed shape is the
53
+ * common case for any schema past the simplest, and this is its worked example;
54
+ * `transcription-workflow` is the all-declared one.
55
+ */
56
+ export const redline = workflow({
57
+ description: "Write a piece from a brief, then critique and revise it until it is worth shipping",
58
+ input: z.object({
59
+ // Short on purpose: it renders as a one-line control, and a brief that
60
+ // needs three paragraphs is the `mustCover` list wearing a disguise.
61
+ brief: z.string().min(20).max(400).describe("One sentence: what to write, and why"),
62
+ audience: z
63
+ .enum(["general readers", "engineers", "executives", "customers"])
64
+ .describe("Who it is for"),
65
+ // A `z.enum` is what makes the control above a `<SelectField>` rather than a
66
+ // text box — the form is as good as the schema is specific.
67
+ rounds: z
68
+ .number()
69
+ .int()
70
+ .min(1)
71
+ .max(MAX_ROUNDS)
72
+ .default(2)
73
+ .describe("How many critique-and-revise rounds to allow"),
74
+ mustCover: z
75
+ .array(z.string().max(200))
76
+ .max(6)
77
+ .default([])
78
+ .describe("Points the piece must cover"),
79
+ }),
80
+ run: redlineFlow,
81
+ });
82
+
83
+ export default workflowApp({
84
+ name: "Redline",
85
+ workflows: { redline },
86
+ // Checked at deploy time, so a missing key is a warning naming it rather than
87
+ // a run that fails on its first step.
88
+ requiredEnv: ["ASSEMBLYAI_API_KEY"],
89
+ });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The redline desk's page: a form, the loop turning, and the piece it produced.
3
+ *
4
+ * `link-digest` shows the workflow primitives raw and `transcription-workflow` shows
5
+ * the form layer over them; this page is the MIXED case, which is what most real
6
+ * schemas need and what neither of those has.
7
+ *
8
+ * ## Half of this form is declared and half is written
9
+ *
10
+ * `<WorkflowFields>` renders one control per SCALAR property of the workflow's
11
+ * own input schema, read from `GET /workflows` — so the brief, the audience
12
+ * picker (a `<SelectField>`, because `agent.ts` declares a `z.enum`) and the
13
+ * rounds spinner exist here because of what the schema says, and adding a fourth
14
+ * scalar adds a fourth control with no edit to this file.
15
+ *
16
+ * It renders nothing for `mustCover`, deliberately: that property is an ARRAY,
17
+ * and there is no honest generic control for one. So this page writes that field
18
+ * itself — a plain `<TextAreaField>` in the same `<Form>`, one point per line —
19
+ * and maps it on submit. Every field in `@alexkroman1/aai-ui` is a plain named
20
+ * control, which is what lets the declared and the hand-written ones sit
21
+ * together and arrive as one object.
22
+ *
23
+ * The mapping is the other half of that: `<Form>` collects what the DOM holds,
24
+ * and a textarea holds a string where the workflow's schema wants `string[]`.
25
+ * `toInput` is where the two meet — and it is the only place, so the split lives
26
+ * in one function rather than in the field, the submit handler and the workflow.
27
+ */
28
+
29
+ import "@alexkroman1/aai-ui/styles.css";
30
+ import type { WorkflowOutputOf } from "@alexkroman1/aai";
31
+ import {
32
+ Form,
33
+ type FormValues,
34
+ page,
35
+ SubmitButton,
36
+ TextAreaField,
37
+ useWorkflowSubmit,
38
+ WorkflowFields,
39
+ WorkflowProgress,
40
+ type WorkflowRun,
41
+ } from "@alexkroman1/aai-ui";
42
+ import type { redline } from "./agent.ts";
43
+
44
+ /**
45
+ * What a finished run reports.
46
+ *
47
+ * Derived from the workflow declaration rather than restated — `import type` is
48
+ * erased, so naming `redline` here bundles none of the agent, the SDK, or the
49
+ * workflow body into this page.
50
+ */
51
+ type Redline = WorkflowOutputOf<typeof redline>;
52
+
53
+ /** The workflow this page drives. Matches the key in `workflowApp({ workflows })`. */
54
+ const WORKFLOW = "redline";
55
+
56
+ /**
57
+ * The submitted form as the workflow's input schema wants it.
58
+ *
59
+ * One function, because the textarea-to-array split is exactly the kind of
60
+ * thing that otherwise gets half-done in three places. Blank lines go, so a
61
+ * trailing newline is not a requirement to cover "".
62
+ */
63
+ export function toInput(values: FormValues): FormValues {
64
+ const raw = typeof values.mustCover === "string" ? values.mustCover : "";
65
+ return {
66
+ ...values,
67
+ mustCover: raw
68
+ .split("\n")
69
+ .map((line) => line.trim())
70
+ .filter((line) => line.length > 0),
71
+ };
72
+ }
73
+
74
+ function RedlineDesk() {
75
+ const { submit, run, pending, error, reset } = useWorkflowSubmit<Redline>(WORKFLOW);
76
+
77
+ return (
78
+ <main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
79
+ <header className="flex flex-col gap-1">
80
+ <h1 className="text-2xl font-medium">Redline</h1>
81
+ <p className="text-sm opacity-70">
82
+ Give it a brief. It writes a draft, grades the draft against the brief, revises, and goes
83
+ round again until the critic would ship it or the rounds run out.
84
+ </p>
85
+ </header>
86
+
87
+ <Form onSubmit={(values) => submit(toInput(values))} error={error}>
88
+ {/* The scalars: brief, audience, rounds. Declared, not written. */}
89
+ <WorkflowFields workflow={WORKFLOW} />
90
+ {/* The array the schema declares and no generic control can render. */}
91
+ <TextAreaField
92
+ name="mustCover"
93
+ label="Must cover"
94
+ hint="One point per line. Leave empty if nothing is required."
95
+ rows={3}
96
+ />
97
+ <SubmitButton pending={pending}>Write it</SubmitButton>
98
+ </Form>
99
+
100
+ {run && <RunPanel run={run} onClear={reset} />}
101
+ </main>
102
+ );
103
+ }
104
+
105
+ /** The critique trail: what each round objected to, and what the score was. */
106
+ function Rounds({ rounds }: { rounds: Redline["rounds"] }) {
107
+ if (rounds.length === 0) return null;
108
+ return (
109
+ <ol className="flex flex-col gap-3">
110
+ {rounds.map((entry) => (
111
+ <li key={entry.round} className="flex flex-col gap-1 border-l pl-4">
112
+ <p className="text-xs uppercase tracking-[1.2px] opacity-60">
113
+ Round {entry.round} · {entry.critique.score}/10 ·{" "}
114
+ {entry.critique.verdict === "ship" ? "ship it" : "revise"}
115
+ </p>
116
+ <ul className="flex list-disc flex-col gap-1 pl-5 text-sm">
117
+ {entry.critique.notes.map((note) => (
118
+ <li key={note}>{note}</li>
119
+ ))}
120
+ </ul>
121
+ </li>
122
+ ))}
123
+ </ol>
124
+ );
125
+ }
126
+
127
+ /** The run's status, its narration, its critique trail, and the piece. */
128
+ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () => void }) {
129
+ return (
130
+ <section className="flex flex-col gap-4 rounded-md border p-5">
131
+ <div className="flex items-baseline justify-between gap-4">
132
+ <h2 className="text-sm font-medium uppercase tracking-[1.2px]">
133
+ {STATUS_LINE[run.status]}
134
+ </h2>
135
+ <button type="button" onClick={onClear} className="text-xs underline opacity-60">
136
+ Clear
137
+ </button>
138
+ </div>
139
+
140
+ {/* The run's own narration — the complement of the status line, which is
141
+ `running` for a run's whole life, so a one-round redline and a
142
+ three-round one look identical while they happen. These lines come from
143
+ the run itself (`report()` in `workflows/redline.ts`), and they REPLAY,
144
+ so a reload mid-run catches up rather than starting from whatever
145
+ arrives next. */}
146
+ <WorkflowProgress runId={run.runId} />
147
+
148
+ {/* Discriminated on `status`, so `output` and `error` are reachable
149
+ without a cast. */}
150
+ {run.status === "completed" && (
151
+ <>
152
+ <p className="text-xs opacity-60">
153
+ {run.output.words} words · {run.output.roundsRun} round
154
+ {run.output.roundsRun === 1 ? "" : "s"} ·{" "}
155
+ {/* Which of the two stop conditions ended the loop is the one thing
156
+ a reader cannot infer from the round count alone. */}
157
+ {run.output.shipped ? "the critic stopped it" : "the round budget stopped it"}
158
+ </p>
159
+ <Rounds rounds={run.output.rounds} />
160
+ <article className="whitespace-pre-wrap text-sm leading-relaxed">
161
+ {run.output.draft}
162
+ </article>
163
+ </>
164
+ )}
165
+ {run.status === "failed" && <p className="text-red-600">{run.error}</p>}
166
+ </section>
167
+ );
168
+ }
169
+
170
+ /**
171
+ * One line describing where a run has got to.
172
+ *
173
+ * A `Record` keyed by the status union rather than a switch, so a status added
174
+ * to the SDK is a compile error here instead of falling through a `default:`.
175
+ */
176
+ const STATUS_LINE: Record<WorkflowRun["status"], string> = {
177
+ pending: "Queued",
178
+ running: "Writing…",
179
+ completed: "Done",
180
+ failed: "Failed",
181
+ cancelled: "Cancelled",
182
+ };
183
+
184
+ page({ name: "Redline", component: RedlineDesk });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The writer, the critic and the reviser — and where they come from.
3
+ *
4
+ * **Adapted from LangGraph's reflection tutorial** (MIT,
5
+ * <https://github.com/langchain-ai/langgraph>,
6
+ * `docs/docs/tutorials/reflection/reflection.ipynb` — the essay assistant), with
7
+ * the structured critique from its Reflexion sibling.
8
+ *
9
+ * | reflection | here |
10
+ * | --- | --- |
11
+ * | `generation_node` (essay assistant) | {@link WRITER_SYSTEM} |
12
+ * | `reflection_node` (teacher grading a submission) | {@link CRITIC_SYSTEM} |
13
+ * | the generation node re-run with the critique | {@link REVISER_SYSTEM} |
14
+ * | `should_continue` (stop after N messages) | the `rounds` input, PLUS a critic that may stop early |
15
+ *
16
+ * **The critic decides when to stop, and that is the one real addition.** Their
17
+ * loop ends on a message count: three rounds is three rounds, whether the second
18
+ * draft was already good or the fourth would have been the one that landed. A
19
+ * fixed count spends the same money on a piece that was fine and gives up on one
20
+ * that was not. So {@link CRITIC_SYSTEM} asks for a VERDICT alongside the
21
+ * notes — and because that verdict comes back as a step's journaled result, the
22
+ * loop it controls is replay-stable (see `redline.ts`).
23
+ *
24
+ * The rest is theirs, and the part worth keeping verbatim in spirit is the
25
+ * critic's stance: it is grading a submission, not co-writing it. A critic
26
+ * prompted to "improve this" rewrites the piece in its own voice and the loop
27
+ * stops converging on the brief.
28
+ *
29
+ * A prompt is DATA, so this module carries no directive and the builder leaves
30
+ * it alone.
31
+ */
32
+
33
+ /** Their `generation_node`, retargeted from five-paragraph essays to a brief. */
34
+ export const WRITER_SYSTEM = [
35
+ "You are a writing assistant. Write the best piece you can for the brief you",
36
+ "are given, for the audience named in it.",
37
+ "Cover every point the brief says must be covered — those are requirements,",
38
+ "not suggestions.",
39
+ "Write prose, not an outline: no headings unless the brief asks for them, no",
40
+ "bullet lists standing in for paragraphs.",
41
+ "Return the piece alone, with no preamble and no note about what you did.",
42
+ ].join(" ");
43
+
44
+ /** Their `reflection_node`: a teacher grading a submission, not a co-author. */
45
+ export const CRITIC_SYSTEM = [
46
+ "You are grading a submission against its brief. Generate critique and",
47
+ "recommendations: be specific about length, depth, structure, evidence and",
48
+ "style, and say what is MISSING as well as what is weak.",
49
+ "Do not rewrite the piece — recommend, so the writer revises in their own",
50
+ "voice.",
51
+ "Then give a verdict. 'ship' means a reader would be well served by this as",
52
+ "it stands; 'revise' means at least one of your notes is worth another pass.",
53
+ "Be willing to say 'ship': a piece that is already good does not improve by",
54
+ "being sent round again, and every extra round costs the person waiting.",
55
+ 'Reply as JSON only: {"verdict": "ship" | "revise", "score": number 1-10,',
56
+ '"notes": string[]}. Three notes at most, ordered by how much they matter.',
57
+ "No markdown fence, no preamble.",
58
+ ].join(" ");
59
+
60
+ /** Their generation node re-entered with the critique in the conversation. */
61
+ export const REVISER_SYSTEM = [
62
+ "You are revising your own piece against a critique of it.",
63
+ "Address every note. Where you disagree with one, address the underlying",
64
+ "concern rather than ignoring it.",
65
+ "Keep what already worked — a revision that rewrites the whole piece has",
66
+ "thrown away the part the critic did not object to.",
67
+ "Return the revised piece alone, with no preamble and no changelog.",
68
+ ].join(" ");