@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,248 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * The durable half of the link digest: a `"use workflow"` body and its steps,
4
+ * and the steps really read the page and really call a model.
5
+ *
6
+ * The rules are the same ones `research-workflow/workflows/research.ts` spells out
7
+ * — the body is replayed from the top on every resume, so it holds no live
8
+ * handle and makes no undurable decision, and a step's arguments and return
9
+ * value cross a queue and so must be JSON-shaped and small. Read that file for
10
+ * the full argument, including where the API key comes from (a step is handed no
11
+ * `ToolContext`, so `requireStepEnv` is what makes an authenticated call
12
+ * possible at all). What THIS one adds is the case where NOBODY IS WAITING.
13
+ *
14
+ * A voice agent's run is started by a caller who is on the line. This one is
15
+ * started by a form: the page has the `runId` and the tab may be closed the
16
+ * instant after, which is exactly why the surface is an HTTP API over a durable
17
+ * run rather than a request that holds a socket open.
18
+ *
19
+ * ## Two steps, because they fail differently
20
+ *
21
+ * `fetchArticle` reaches a stranger's web server; `summarize` reaches the model.
22
+ * Splitting them is what makes a rate-limited model call replay the FETCH from
23
+ * the journal instead of hammering someone's site again — and it is why the
24
+ * fetched text crosses a queue between them, which is what the cap on it is for.
25
+ */
26
+
27
+ import { throwStepError, toStepError } from "@alexkroman1/aai/step-errors";
28
+ import { report, stepFetch, stepGenerateJson } from "@alexkroman1/aai/utils";
29
+ import { FatalError, sleep } from "workflow";
30
+ import { z } from "zod";
31
+
32
+ /** How long the digest sits before it is filed, so the wait is visible in dev. */
33
+ const SETTLE = "10 seconds";
34
+
35
+ /**
36
+ * Characters of article text carried between the two steps.
37
+ *
38
+ * The cap is the pass-an-id-not-a-payload rule meeting a case where the payload
39
+ * IS the work: the text has to cross the queue, so it is bounded rather than
40
+ * trusted. 24k characters is several thousand words — past where an extra page
41
+ * of boilerplate changes the summary.
42
+ */
43
+ const MAX_ARTICLE_CHARS = 24_000;
44
+
45
+ /** Points the digest reduces a page to. */
46
+ const POINTS = 3;
47
+
48
+ /** The page fetch's deadline. HTTP has none of its own, and a hung step never ends. */
49
+ const FETCH_TIMEOUT_MS = 30_000;
50
+
51
+ /** What one digest pass produces. Small and JSON-shaped, like every step result. */
52
+ export type Digest = {
53
+ url: string;
54
+ headline: string;
55
+ points: string[];
56
+ };
57
+
58
+ /**
59
+ * The shape the model is asked for, as something that CHECKS rather than
60
+ * something the compiler is told to believe.
61
+ *
62
+ * `stepGenerateJson` validates against this and throws plainly when the reply
63
+ * misses — which is what a retry is for, since a model that answered with prose
64
+ * may well obey on the next attempt. Before the SDK took the schema, this was a
65
+ * hand-written `isDigestShape` guard beside a hand-written fence stripper.
66
+ */
67
+ const DigestReply = z.object({
68
+ headline: z.string().trim().min(1),
69
+ points: z.array(z.string()).min(1),
70
+ });
71
+
72
+ /** What the page actually said, on its way from one step to the next. */
73
+ export type Article = {
74
+ url: string;
75
+ /** The `<title>`, or the hostname when there is none. */
76
+ title: string;
77
+ /** Readable text, capped — see {@link MAX_ARTICLE_CHARS}. */
78
+ text: string;
79
+ };
80
+
81
+ /**
82
+ * Fetch a link, summarize it, and file the result.
83
+ *
84
+ * Whatever this returns is what a completed run reports as `output` — so it is
85
+ * literally the page's render model, and `WorkflowOutputOf<typeof digest>` in
86
+ * `client.tsx` is that type, derived rather than restated.
87
+ */
88
+ export async function digestFlow(input: { url: string }) {
89
+ "use workflow";
90
+
91
+ const article = await fetchArticle(input.url);
92
+ const digest = await summarize(article);
93
+
94
+ // Suspended, not blocked: the sandbox is free to exit here and the run
95
+ // resumes when it comes due. Nothing about the code changes if it is
96
+ // `"6 hours"` — which is the interesting version, and the one that makes an
97
+ // overnight digest a digest rather than a slow request.
98
+ await sleep(SETTLE);
99
+
100
+ return { ...digest, filedAt: await file(digest) };
101
+ }
102
+
103
+ /**
104
+ * Read the page.
105
+ *
106
+ * A step, so it runs once per successful execution and its result is journaled;
107
+ * a replay returns that result instead of fetching again — which matters here
108
+ * more than usual, because the far side is somebody else's web server.
109
+ */
110
+ export async function fetchArticle(url: string): Promise<Article> {
111
+ "use step";
112
+
113
+ const { hostname } = new URL(url);
114
+ await report(`Reading ${hostname}…`);
115
+
116
+ // `stepFetch`, not `fetch`, and the rule has no exception for a step that
117
+ // makes only one request: the global pins nothing, so it offers h2 in ALPN
118
+ // and a server that takes it multiplexes every concurrent request from this
119
+ // process onto ONE connection — and a capacity limit then arrives as a stream
120
+ // reset with no HTTP status, which `toStepError` below has nothing to read.
121
+ // It also reports a connection failure with its whole `cause` chain instead
122
+ // of a bare `TypeError: fetch failed`. Redirects are followed by default.
123
+ const response = await stepFetch(url, {
124
+ // Some sites answer a bare request with a challenge page; asking for HTML
125
+ // at least says what we want. Nothing here defeats a real bot wall, and a
126
+ // template pretending otherwise would be the dishonest version.
127
+ headers: { Accept: "text/html,application/xhtml+xml" },
128
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
129
+ });
130
+ // The retryable/terminal split, for the page we were pointed at: a 404 or a
131
+ // 403 answers the same way on the fourth attempt, while a rate limit is
132
+ // exactly what retries are for — and `toStepError` is what carries its
133
+ // `Retry-After` into the DevKit's schedule instead of the default backoff.
134
+ if (!response.ok) throw toStepError(response, `GET ${url} failed: HTTP ${response.status}`);
135
+
136
+ const html = await response.text();
137
+ const text = extractText(html);
138
+ if (text.length < 200) {
139
+ // Not transient: the same URL returns the same near-empty page on a retry.
140
+ // A JS-rendered site is the usual cause and no number of attempts fixes it.
141
+ // A direct `FatalError`, not `throwFatalStepError`: nothing is being
142
+ // classified here and there is no cause to preserve — this step has simply
143
+ // decided, and the helper is for the `catch` block where the linter would
144
+ // (rightly) want the original error kept.
145
+ throw new FatalError(`${hostname} returned no readable text — is the page rendered in JS?`);
146
+ }
147
+ return { url, title: extractTitle(html) ?? hostname, text };
148
+ }
149
+
150
+ /**
151
+ * Reduce the page to a headline and a few points.
152
+ *
153
+ * Separate from the fetch on purpose: a rate-limited model call replays the
154
+ * fetch from the journal rather than hitting a stranger's server again. The
155
+ * whole Node runtime is available here, unlike in the body.
156
+ */
157
+ export async function summarize(article: Article): Promise<Digest> {
158
+ "use step";
159
+
160
+ await report("Pulling out the claims worth keeping.");
161
+
162
+ // `stepGenerateJson` unwraps the fence a model puts around JSON, parses it,
163
+ // and validates it against `DigestReply` — and throws PLAINLY when any of
164
+ // those misses, which is the whole retry policy in one distinction: a model
165
+ // that answered with prose may answer correctly on the next attempt, where a
166
+ // 401 will not. `throwStepError` is what makes the 401 half terminal.
167
+ const parsed = await stepGenerateJson(
168
+ `Title: ${article.title}\nURL: ${article.url}\n\n${article.text}`,
169
+ {
170
+ schema: DigestReply,
171
+ system:
172
+ `You digest articles. Reply with JSON only: {"headline": string, "points": string[]}. ` +
173
+ `Give exactly ${POINTS} points. No markdown fence, no preamble.`,
174
+ },
175
+ ).catch(throwStepError);
176
+
177
+ return {
178
+ url: article.url,
179
+ headline: parsed.headline,
180
+ points: parsed.points.slice(0, POINTS),
181
+ };
182
+ }
183
+
184
+ /** A rate limit — and a model that ignored the format — are both expected here. */
185
+ summarize.maxRetries = 5;
186
+
187
+ /**
188
+ * File the digest.
189
+ *
190
+ * Separate from `summarize` on purpose: two steps mean a crash between them
191
+ * replays the expensive half for free and re-issues only the cheap one.
192
+ * Returning the timestamp rather than reading a clock in the BODY is the same
193
+ * rule — a step's result is journaled and therefore stable across replays,
194
+ * where `Date.now()` in the body would change on every one.
195
+ */
196
+ export async function file(_digest: Digest): Promise<string> {
197
+ "use step";
198
+
199
+ await report("Filing the digest.");
200
+ // A real desk would write the digest to its database here. The stub writes
201
+ // nothing, which is what the `_` says — and it is a stub because `ctx.db` is
202
+ // the half of a tool context a step still cannot reach.
203
+ return new Date().toISOString();
204
+ }
205
+
206
+ // ---- Pure helpers -----------------------------------------------------------
207
+
208
+ /** The document's `<title>`, when it has one. */
209
+ export function extractTitle(html: string): string | undefined {
210
+ const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];
211
+ return title ? decodeEntities(title).replace(/\s+/g, " ").trim() || undefined : undefined;
212
+ }
213
+
214
+ /**
215
+ * Reduce HTML to the text a model should read.
216
+ *
217
+ * Deliberately crude, and the crudeness is the honest part: a real extractor is
218
+ * a readability implementation and a dependency, where this is four `replace`
219
+ * calls that get most of an article. What it MUST do is drop `<script>` and
220
+ * `<style>` CONTENT — stripping tags alone leaves a page's JavaScript in the
221
+ * prompt, which is both expensive and a way to smuggle instructions past the
222
+ * reader.
223
+ */
224
+ export function extractText(html: string): string {
225
+ return decodeEntities(
226
+ html
227
+ .replace(/<(script|style|noscript|template)[^>]*>[\s\S]*?<\/\1>/gi, " ")
228
+ .replace(/<!--[\s\S]*?-->/g, " ")
229
+ .replace(/<[^>]+>/g, " "),
230
+ )
231
+ .replace(/\s+/g, " ")
232
+ .trim()
233
+ .slice(0, MAX_ARTICLE_CHARS);
234
+ }
235
+
236
+ /** The five entities that survive tag-stripping often enough to matter. */
237
+ function decodeEntities(text: string): string {
238
+ return (
239
+ text
240
+ .replace(/&lt;/g, "<")
241
+ .replace(/&gt;/g, ">")
242
+ .replace(/&quot;/g, '"')
243
+ .replace(/&#0?39;|&apos;/g, "'")
244
+ .replace(/&nbsp;/g, " ")
245
+ // `&amp;` LAST, or `&amp;lt;` decodes twice into a `<` the page never had.
246
+ .replace(/&amp;/g, "&")
247
+ );
248
+ }
@@ -1,6 +1,5 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
2
  import { assemblyAILlm } from "@alexkroman1/aai/llm";
3
- import systemPrompt from "./system-prompt.md?raw";
4
3
 
5
4
  export default agent({
6
5
  name: "Math Buddy",
@@ -10,7 +9,6 @@ export default agent({
10
9
  // and TTS stay on the AssemblyAI defaults. (The string shorthand
11
10
  // `llm: "gemini-2.5-flash-lite"` desugars to this same descriptor.)
12
11
  llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
13
- systemPrompt,
14
12
  greeting:
15
13
  "Hey, I'm Math Buddy. Try asking me something like, what's 127 times 849, convert 5 miles to kilometers, or roll 3 twenty-sided dice.",
16
14
  builtinTools: ["run_code"],
@@ -0,0 +1,80 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ import { toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
+ import { describe, expect, test } from "vitest";
6
+ import authoredAgent from "./agent.ts";
7
+ import { CATEGORIES, MOODS } from "./shared.ts";
8
+
9
+ /**
10
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
11
+ *
12
+ * The glob is written HERE rather than reached for from a shared helper because
13
+ * this file SHIPS — a scaffolded project has no repo helper to import.
14
+ */
15
+ const agentDef = withDiscoveredTools(
16
+ authoredAgent,
17
+ import.meta.glob("./tools/*.ts", { eager: true }),
18
+ );
19
+
20
+ describe("night-owl template", () => {
21
+ test("config passes manifest validation", () => {
22
+ // Same conversion `aai build`/`aai deploy` run.
23
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
24
+ });
25
+
26
+ test("declares run_code, which is the other half of the template", () => {
27
+ // The sleep-time arithmetic is the builtin's job, not a tool's — that split
28
+ // IS this template's subject, so a dropped builtin leaves the agent doing
29
+ // mental arithmetic out loud.
30
+ expect(agentDef.builtinTools).toContain("run_code");
31
+ });
32
+
33
+ test("recommend is discovered from tools/", () => {
34
+ expect(Object.keys(agentDef.tools ?? {})).toEqual(["recommend"]);
35
+ });
36
+ });
37
+
38
+ describe("recommend", () => {
39
+ test("answers with picks for the category and mood asked for", async () => {
40
+ const ctx = createToolContext();
41
+ const result = await runTool(agentDef, "recommend", { category: "movie", mood: "cozy" }, ctx);
42
+ expect(result).toMatchObject({ category: "movie", mood: "cozy" });
43
+ expect((result as { picks: string[] }).picks.length).toBeGreaterThan(0);
44
+ });
45
+
46
+ test("pushes the same picks to the client, which is what the page renders", async () => {
47
+ // `ctx.send` is the only reason this tool takes a context at all: the
48
+ // client renders the picks rather than waiting to hear them read aloud.
49
+ const ctx = createToolContext();
50
+ const result = await runTool(agentDef, "recommend", { category: "book", mood: "spooky" }, ctx);
51
+ expect(ctx.sent).toEqual([{ event: "recommendations", data: result }]);
52
+ });
53
+
54
+ test("every category/mood pair the schema admits has picks behind it", async () => {
55
+ // The table is hand-written and the schema is generated from the same two
56
+ // const arrays, so a category added to `shared.ts` and forgotten in the
57
+ // table is a `TypeError` on the first call — the exact failure this
58
+ // package's guide records three shipped tools having.
59
+ for (const category of CATEGORIES) {
60
+ for (const mood of MOODS) {
61
+ const result = await runTool(
62
+ agentDef,
63
+ "recommend",
64
+ { category, mood },
65
+ createToolContext(),
66
+ );
67
+ expect((result as { picks: string[] }).picks, `${category}/${mood}`).not.toHaveLength(0);
68
+ }
69
+ }
70
+ });
71
+
72
+ test("a mood outside the enum is refused by the schema", async () => {
73
+ // The wire boundary: an LLM tool call is untyped, so the schema is the only
74
+ // thing between a hallucinated mood and an index into `undefined`.
75
+ const schema = agentDef.tools?.recommend?.inputSchema;
76
+ if (!schema) throw new Error("recommend has no input schema");
77
+ const bad = await schema["~standard"].validate({ category: "movie", mood: "melancholy" });
78
+ expect(bad.issues).toBeDefined();
79
+ });
80
+ });
@@ -1,75 +1,8 @@
1
- import { agent, tool } from "@alexkroman1/aai";
2
- import { z } from "zod";
3
- import { CATEGORIES, type Category, MOODS, type Mood } from "./shared.ts";
4
-
5
- const PICKS: Record<Category, Record<Mood, string[]>> = {
6
- movie: {
7
- chill: ["Lost in Translation", "The Grand Budapest Hotel", "Amelie"],
8
- intense: ["Inception", "Interstellar", "The Dark Knight"],
9
- cozy: ["When Harry Met Sally", "The Holiday", "Paddington 2"],
10
- spooky: ["The Shining", "Get Out", "Hereditary"],
11
- funny: ["The Big Lebowski", "Airplane!", "Superbad"],
12
- },
13
- music: {
14
- chill: ["Khruangbin — Con Todo El Mundo", "Tycho — Dive", "Bonobo — Migration"],
15
- intense: ["Radiohead — OK Computer", "Tool — Lateralus", "Deftones — White Pony"],
16
- cozy: [
17
- "Norah Jones — Come Away with Me",
18
- "Iron & Wine — Our Endless Numbered Days",
19
- "Bon Iver — For Emma, Forever Ago",
20
- ],
21
- spooky: [
22
- "Portishead — Dummy",
23
- "Massive Attack — Mezzanine",
24
- "Boards of Canada — Music Has the Right to Children",
25
- ],
26
- funny: [
27
- "Weird Al — Running with Scissors",
28
- "Flight of the Conchords — S/T",
29
- "Tenacious D — S/T",
30
- ],
31
- },
32
- book: {
33
- chill: ["Norwegian Wood — Murakami", "The Alchemist — Coelho", "Siddhartha — Hesse"],
34
- intense: ["Blood Meridian — McCarthy", "House of Leaves — Danielewski", "Neuromancer — Gibson"],
35
- cozy: [
36
- "The House in the Cerulean Sea — Klune",
37
- "A Man Called Ove — Backman",
38
- "Anxious People — Backman",
39
- ],
40
- spooky: [
41
- "The Haunting of Hill House — Jackson",
42
- "Mexican Gothic — Moreno-Garcia",
43
- "The Turn of the Screw — James",
44
- ],
45
- funny: ["Good Omens — Pratchett & Gaiman", "Hitchhiker's Guide — Adams", "Catch-22 — Heller"],
46
- },
47
- };
1
+ import { agent } from "@alexkroman1/aai";
48
2
 
49
3
  export default agent({
50
4
  name: "Night Owl",
51
- systemPrompt:
52
- "You are Night Owl, a cozy evening companion. You help people wind down, recommend entertainment, and share interesting facts about the night sky. Keep your tone warm and relaxed. Use short, conversational responses.\n\nUse run_code for sleep calculations:\n- Each sleep cycle is 90 minutes, plus 15 minutes to fall asleep\n- Bedtime = wake_time - (cycles * 90 + 15) minutes\n- If result is negative, add 1440 (24 hours in minutes)\n- Format as HH:MM",
53
5
  greeting:
54
6
  "Hey there, night owl. Try asking me for a cozy movie recommendation, or tell me what time you need to wake up and I'll calculate the best time to fall asleep.",
55
7
  builtinTools: ["run_code"],
56
-
57
- tools: {
58
- recommend: tool({
59
- description: "Get recommendations for movies, music, or books based on mood.",
60
- inputSchema: z.object({
61
- category: z.enum(CATEGORIES),
62
- mood: z.enum(MOODS),
63
- }),
64
- async execute(args, ctx) {
65
- const result = {
66
- category: args.category,
67
- mood: args.mood,
68
- picks: PICKS[args.category][args.mood],
69
- };
70
- ctx.send("recommendations", result);
71
- return result;
72
- },
73
- }),
74
- },
75
8
  });
@@ -0,0 +1,10 @@
1
+ You are Night Owl, a cozy evening companion. You help people wind down,
2
+ recommend entertainment, and share interesting facts about the night sky. Keep
3
+ your tone warm and relaxed. Use short, conversational responses.
4
+
5
+ Use run_code for sleep calculations:
6
+
7
+ - Each sleep cycle is 90 minutes, plus 15 minutes to fall asleep
8
+ - Bedtime = wake_time - (cycles \* 90 + 15) minutes
9
+ - If result is negative, add 1440 (24 hours in minutes)
10
+ - Format as HH:MM
@@ -0,0 +1,64 @@
1
+ import { tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { CATEGORIES, type Category, MOODS, type Mood } from "../shared.ts";
4
+
5
+ const PICKS: Record<Category, Record<Mood, string[]>> = {
6
+ movie: {
7
+ chill: ["Lost in Translation", "The Grand Budapest Hotel", "Amelie"],
8
+ intense: ["Inception", "Interstellar", "The Dark Knight"],
9
+ cozy: ["When Harry Met Sally", "The Holiday", "Paddington 2"],
10
+ spooky: ["The Shining", "Get Out", "Hereditary"],
11
+ funny: ["The Big Lebowski", "Airplane!", "Superbad"],
12
+ },
13
+ music: {
14
+ chill: ["Khruangbin — Con Todo El Mundo", "Tycho — Dive", "Bonobo — Migration"],
15
+ intense: ["Radiohead — OK Computer", "Tool — Lateralus", "Deftones — White Pony"],
16
+ cozy: [
17
+ "Norah Jones — Come Away with Me",
18
+ "Iron & Wine — Our Endless Numbered Days",
19
+ "Bon Iver — For Emma, Forever Ago",
20
+ ],
21
+ spooky: [
22
+ "Portishead — Dummy",
23
+ "Massive Attack — Mezzanine",
24
+ "Boards of Canada — Music Has the Right to Children",
25
+ ],
26
+ funny: [
27
+ "Weird Al — Running with Scissors",
28
+ "Flight of the Conchords — S/T",
29
+ "Tenacious D — S/T",
30
+ ],
31
+ },
32
+ book: {
33
+ chill: ["Norwegian Wood — Murakami", "The Alchemist — Coelho", "Siddhartha — Hesse"],
34
+ intense: ["Blood Meridian — McCarthy", "House of Leaves — Danielewski", "Neuromancer — Gibson"],
35
+ cozy: [
36
+ "The House in the Cerulean Sea — Klune",
37
+ "A Man Called Ove — Backman",
38
+ "Anxious People — Backman",
39
+ ],
40
+ spooky: [
41
+ "The Haunting of Hill House — Jackson",
42
+ "Mexican Gothic — Moreno-Garcia",
43
+ "The Turn of the Screw — James",
44
+ ],
45
+ funny: ["Good Omens — Pratchett & Gaiman", "Hitchhiker's Guide — Adams", "Catch-22 — Heller"],
46
+ },
47
+ };
48
+
49
+ export default tool({
50
+ description: "Get recommendations for movies, music, or books based on mood.",
51
+ inputSchema: z.object({
52
+ category: z.enum(CATEGORIES),
53
+ mood: z.enum(MOODS),
54
+ }),
55
+ async execute(args, ctx) {
56
+ const result = {
57
+ category: args.category,
58
+ mood: args.mood,
59
+ picks: PICKS[args.category][args.mood],
60
+ };
61
+ ctx.send("recommendations", result);
62
+ return result;
63
+ },
64
+ });
@@ -1,9 +1,7 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
- import systemPrompt from "./system-prompt.md?raw";
3
2
 
4
3
  export default agent({
5
4
  name: "Penny",
6
- systemPrompt,
7
5
  greeting:
8
6
  "Hey, I'm Penny, your personal finance helper. Try asking me something like, what's 100 dollars in euros, what's the price of bitcoin, or help me split a 120 dollar bill four ways with 20 percent tip.",
9
7
  builtinTools: ["run_code", "fetch_json"],