@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
@@ -28,7 +28,16 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
28
28
  `aai init --template <name>` scaffolds a fresh project from one. Closest
29
29
  matches: `simple`, `pipeline-simple`, `web-researcher`, `solo-rpg`,
30
30
  `pizza-ordering`, `retail` (the most complex — 15 tools over a
31
- relational store, with a `syncState`-driven UI). When reading SDK
31
+ relational store, with a `syncState`-driven UI). Four are ports of agents you
32
+ may already know from LangChain/LangGraph, and each says in its own source
33
+ what had to change: `travel-concierge` (their customer-support bot —
34
+ specialist desks, and every booking staged for a spoken confirmation before
35
+ it applies), `support-line` (self-RAG/CRAG — retrieve, grade what came back,
36
+ rewrite the question, and refuse to speak an answer it cannot ground),
37
+ `plan-and-execute` (plan then work the plan — one step per tool call, so the
38
+ caller can redirect between them), and `redline` (the reflection agent — write,
39
+ critique, revise, which is too slow for a phone and so is a PAGE over a
40
+ durable run rather than a voice agent). When reading SDK
32
41
  types under
33
42
  `node_modules/@alexkroman1/aai*/dist/`, note the built entry points
34
43
  re-export with source specifiers (`"./sdk/constants.ts"`,
@@ -56,22 +65,29 @@ are CLI-only.
56
65
 
57
66
  ## Running it yourself (`npm start`)
58
67
 
59
- `server.mjs` serves this agent from a plain Node process — no CLI, no
60
- bundler, no platform account. It is the deployment counterpart of `aai dev`:
68
+ `server.mjs` serves this agent from a plain Node process — no platform
69
+ account, nothing managed. It is the deployment counterpart of `aai dev`:
61
70
 
62
71
  ```sh
63
72
  npm start # http://127.0.0.1:3000
64
73
  PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
65
74
  ```
66
75
 
76
+ `npm start` **builds first** (that is the `prestart` script) and then serves
77
+ the result: `server.mjs` boots `.aai/worker.mjs`, the same artifact
78
+ `aai publish` uploads. The build is what makes `tools/` work — a tool is
79
+ registered by existing, and the enumeration happens where the bundle is
80
+ assembled, so a server that loaded `agent.ts` directly would run an agent with
81
+ none of its tools. The same build produces your `client.tsx`, so a custom UI is
82
+ served with no extra step.
83
+
67
84
  Secrets work the same as everywhere else: `ctx.env` holds the keys declared
68
85
  in `.env` (or `.env.example`), and a real environment variable of that name
69
86
  wins — so `docker run -e MY_API_KEY=…` needs no `.env` in the image.
70
87
 
71
- Two things to know. It binds **loopback by default**, because this server has
88
+ One thing to know: it binds **loopback by default**, because this server has
72
89
  no request authentication of its own; set `HOST=0.0.0.0` only behind your own
73
- proxy or auth. And with a custom `client.tsx`, run `npm run build` first —
74
- otherwise it serves the default UI and says so at startup.
90
+ proxy or auth.
75
91
 
76
92
  Deleting `server.mjs` costs nothing: `aai dev`, `aai publish` and the managed
77
93
  platform never read it. `run_code` is the one feature that does not follow —
@@ -85,8 +101,9 @@ my-agent/
85
101
  agent.test.ts # Unit tests (optional)
86
102
  client.tsx # Custom UI (optional, React)
87
103
  shared.ts # Types shared between agent.ts and client.tsx
88
- system-prompt.md # Long system prompts (optional, imported)
89
- tools/ # Tool files when too large for inline (optional)
104
+ system-prompt.md # The system prompt — discovered, not imported
105
+ tools/ # One file per tool this is how a tool is declared
106
+ workflows/ # Durable workflow bodies (optional — see "Workflow apps")
90
107
  package.json
91
108
  tsconfig.json
92
109
  .env # Local dev secrets (gitignored)
@@ -99,8 +116,9 @@ import { agent } from "@alexkroman1/aai";
99
116
 
100
117
  export default agent({
101
118
  name: string; // required — display name
102
- systemPrompt?: string; // default: general voice assistant
103
- // (`system` is an accepted alias)
119
+ systemPrompt?: string; // usually ABSENT write system-prompt.md
120
+ // instead; declare it only to COMPOSE
121
+ // one (`system` is an accepted alias)
104
122
  greeting?: string; // default: "Hey there..."
105
123
  voice?: string; // TTS voice for the default pipeline, e.g. "michael"
106
124
  // (shorthand for tts: assemblyAITts({ voice });
@@ -111,7 +129,8 @@ export default agent({
111
129
  s2s?: S2sProvider; // explicit opt-in to speech-to-speech mode
112
130
  sttPrompt?: string; // STT guidance for jargon/acronyms
113
131
  builtinTools?: BuiltinTool[]; // see built-in tools table
114
- tools?: Record<string, ToolDef>;
132
+ // (there is no `tools` field — a tool is a FILE;
133
+ // see "A file in tools/ IS a tool")
115
134
  maxSteps?: number; // default: 10 — max tool calls per turn
116
135
  toolChoice?: ToolChoice; // "auto" (default) | "required" | "none"
117
136
  // | { type: "tool", toolName }
@@ -122,10 +141,8 @@ export default agent({
122
141
  interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 500; 0 disables)
123
142
  deadAirCoverMs?: number; // pipeline only — speak a short filler after this much silence in a turn (default 5000; 0 disables)
124
143
  resumeFalseInterruption?: boolean; // pipeline only — resume an interrupted reply if no user turn commits (default true)
125
- preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default true; false opts out)
126
- state?: () => S; // per-session mutable state, exposed as ctx.state
127
- // (S is inferred; see "Typing ctx.state")
128
- syncState?: (state: S) => unknown; // push a projection of state to the client
144
+ preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default false; true opts in)
145
+ syncState?: StateProjection; // show a slot to the client: slot.projection(view)
129
146
  // (read it with useAgentState; see UI hooks)
130
147
  });
131
148
  ```
@@ -180,21 +197,347 @@ you want the three stages visible in the config or EU data residency across
180
197
  STT and the LLM gateway. Speech-to-speech (S2S) mode is an explicit opt-in
181
198
  via the `s2s` field — see below.
182
199
 
183
- System prompt from file:
200
+ ### `system-prompt.md` IS the system prompt
201
+
202
+ **Write the prompt in `system-prompt.md` beside `agent.ts`, and declare
203
+ nothing.** The build discovers the file, so there is no import line and no
204
+ field — the same rule `tools/` follows, applied to the one part of an agent
205
+ that is a DOCUMENT rather than a value:
184
206
 
185
207
  ```ts
208
+ // agent.ts — nothing about the prompt appears here
209
+ import { agent } from "@alexkroman1/aai";
210
+
211
+ export default agent({ name: "My Agent" });
212
+ ```
213
+
214
+ ```markdown
215
+ <!-- system-prompt.md -->
216
+ You are a concise, friendly assistant.
217
+
218
+ - Keep replies to one or two sentences.
219
+ - Never read a URL aloud.
220
+ ```
221
+
222
+ Why the file rather than a string: a prompt is markdown — paragraphs, headings,
223
+ bulleted lists — and inline it becomes that document spelled as `\n\n` and `\n-`
224
+ escapes inside one string literal, with no wrapping, no preview, and a diff that
225
+ is one line no matter which bullet changed. Editing the prompt is the main loop
226
+ of building an agent, so it should land in the most reviewable place available,
227
+ not the least.
228
+
229
+ Three rules, each a build error naming the file:
230
+
231
+ - **A file nothing reads is an error.** If `system-prompt.md` exists and
232
+ `agent.ts` declares a DIFFERENT `systemPrompt`, the build fails rather than
233
+ ignoring the file — "I edited the prompt and nothing changed" is the failure
234
+ this mechanism exists to prevent.
235
+ - **An empty file is an error**, not a silent fall-through to the framework
236
+ default. Delete the file if that is what you want.
237
+ - **A `system-prompt/` directory is rejected.** One file, no concatenation
238
+ order to guess.
239
+
240
+ **Composing a prompt is still legal, and it is the one case you write the import
241
+ for.** When part of the prompt is computed — a menu, a catalogue, today's date —
242
+ import the file and build the field; the build sees its own text inside your
243
+ prompt and leaves what you built alone:
244
+
245
+ ```ts no-check
246
+ // `no-check`: the prompt file and the menu module are the project's, not this
247
+ // guide's — which is the point of the example.
186
248
  /// <reference types="vite/client" />
187
249
  import { agent } from "@alexkroman1/aai";
188
250
  import systemPrompt from "./system-prompt.md?raw";
189
- export default agent({ name: "My Agent", systemPrompt });
251
+ import { menuText } from "./menu.ts";
252
+
253
+ export default agent({ name: "Pizza", systemPrompt: `${systemPrompt}\n${menuText()}` });
190
254
  ```
191
255
 
256
+ `greeting` stays a field, deliberately: it is one sentence with no structure to
257
+ lose, and it crosses the wire to the browser beside `name` and `page`. **A
258
+ document goes in a file, a value stays in the call.**
259
+
192
260
  **JSON imports need no attribute.** `resolveJsonModule` is on, so
193
261
  `import data from "./knowledge.json"` is all it takes. Do NOT write
194
262
  `assert { type: "json" }` — import assertions were replaced by import
195
263
  attributes and TypeScript rejects them (`TS2880`). If you want to be
196
264
  explicit the modern spelling is `with { type: "json" }`, but plain is fine.
197
265
 
266
+ ## Workflow apps — `workflowApp()`
267
+
268
+ Not every agent's front door is a microphone. When the product is a FORM —
269
+ submit a job, watch it run, read the result — declare it with `workflowApp()`
270
+ instead of `agent()`:
271
+
272
+ ```ts no-check
273
+ import { workflow, workflowApp } from "@alexkroman1/aai";
274
+ import { z } from "zod";
275
+ import { digestFlow } from "./workflows/digest.ts";
276
+
277
+ export const digest = workflow({
278
+ description: "Summarize a link and file the digest",
279
+ input: z.object({ url: z.url().describe("The link to digest") }),
280
+ run: digestFlow,
281
+ });
282
+
283
+ export default workflowApp({
284
+ name: "Link Digest",
285
+ workflows: { digest },
286
+ });
287
+ ```
288
+
289
+ That is the whole declaration, and the fields it does NOT take are the point:
290
+ a workflow app has no session and no LLM loop, so `systemPrompt`, `tools`,
291
+ `maxSteps`, `syncState`, `stt`/`llm`/`tts`/`s2s` and every voice knob
292
+ are **compile errors** here, not fields that quietly do nothing. `greeting` and
293
+ `requiredEnv` stay. `workflowApp()` is `agent({ …, page: "static" })` with the
294
+ discriminant already set — same definition object out, so `aai build`,
295
+ `aai dev` and `aai publish` treat it like any other agent.
296
+
297
+ Reach for it when the user asks for something that outlives a request: an
298
+ overnight job, an upload that takes minutes, anything waiting on a third-party
299
+ callback. Reach for `agent()` when someone is on the line — a voice agent can
300
+ also START a workflow from a tool (`ctx.workflows.start(def, input)`) and
301
+ answer the turn, which is the other shape.
302
+
303
+ **Requires storage** (`aai storage enable`, or `DATABASE_URL` under
304
+ `aai dev`): runs live in the database.
305
+
306
+ ### Workflow bodies live in `workflows/`
307
+
308
+ The build transforms that directory and nothing else. A `"use workflow"` body
309
+ written in `agent.ts` is never transformed — it runs inline once, with no
310
+ durability and nothing saying so.
311
+
312
+ ```ts
313
+ import { sleep } from "workflow";
314
+
315
+ export async function digestFlow(input: { url: string }) {
316
+ "use workflow";
317
+
318
+ const digest = await summarize(input.url);
319
+ // Suspended, not blocked: the container is free to exit here and the run
320
+ // resumes when it comes due. `"6 hours"` works the same as `"10 seconds"`.
321
+ await sleep("10 seconds");
322
+ return { ...digest, filedAt: await file(digest) };
323
+ }
324
+
325
+ async function summarize(url: string) {
326
+ "use step";
327
+ // The whole Node runtime is available in a step: fetch, a model call, a
328
+ // database. Not in the body.
329
+ return { url, headline: `What ${new URL(url).hostname} says`, points: [] };
330
+ }
331
+
332
+ async function file(digest: { url: string }) {
333
+ "use step";
334
+ return new Date().toISOString();
335
+ }
336
+ ```
337
+
338
+ Three rules, all of which fail silently if broken:
339
+
340
+ - **The body replays from the top on every resume**, so it holds no live handle
341
+ and makes no undurable decision — no `Date.now()`, no `Math.random()`, no
342
+ `fetch`. Those belong in a step, whose result is journaled and returned
343
+ unchanged on replay.
344
+ - **A step's arguments and return value cross a queue**, so they must be
345
+ JSON-shaped and small. Put bytes in storage and pass the key.
346
+ - **A step gets no tool context.** It is bundled and dispatched separately from
347
+ the agent, so there is no `ctx` in one — see below for how it reaches the
348
+ agent's env and a model anyway. `ctx.db` has no step-side equivalent yet.
349
+
350
+ ### A step's env, and calling a model from one
351
+
352
+ A step has no `ctx`, so the two things tool code takes for granted come from
353
+ `@alexkroman1/aai/utils` instead. Import them from THERE and not from
354
+ `@alexkroman1/aai` — a `workflows/*.ts` module is bundled separately, and the
355
+ root barrel would drag the whole SDK into that bundle.
356
+
357
+ ```ts no-check
358
+ import { requireStepEnv, stepEnv, StepGenerateError, stepGenerate } from "@alexkroman1/aai/utils";
359
+ import { FatalError } from "workflow";
360
+
361
+ async function summarize(url: string, text: string) {
362
+ "use step";
363
+
364
+ // The agent's env by name — the same values a tool reads from `ctx.env`.
365
+ // `requireStepEnv` fails naming the key; `stepEnv` returns undefined.
366
+ const style = stepEnv("DIGEST_STYLE") ?? "plain";
367
+
368
+ // One model call, on the agent's own ASSEMBLYAI_API_KEY and default model.
369
+ return await stepGenerate(`${style} summary of:\n\n${text}`, {
370
+ system: "Reply with two sentences and nothing else.",
371
+ }).catch(stopOrRetry);
372
+ }
373
+
374
+ // The DevKit retries a step that throws, so decide which failures deserve it.
375
+ // A rate limit does; a bad key does not.
376
+ function stopOrRetry(err: unknown): never {
377
+ if (err instanceof StepGenerateError && !err.retryable) throw new FatalError(err.message);
378
+ throw err;
379
+ }
380
+ ```
381
+
382
+ Two things to know. **The env is what `.env` and `aai secret put` declare** —
383
+ not your shell, even under `aai dev`, so that a step reads the same values
384
+ before and after a deploy. List what you read in `requiredEnv` and a deploy
385
+ checks it for you. And **`stepGenerate` is not `ctx.generate`**: it is one
386
+ request to the AssemblyAI LLM Gateway, with no tools and no structured output,
387
+ because bundling the AI SDK into a step artifact costs megabytes on every
388
+ deploy. Ask it for JSON and parse the reply if you need a shape.
389
+
390
+ ### A step's HTTP: use `stepFetch`, not `fetch`
391
+
392
+ Any outbound request from a step goes through `stepFetch` (also
393
+ `@alexkroman1/aai/utils`). It is not a style preference — `fetch` is the wrong
394
+ call to make from a step, for a reason nothing at the call site shows:
395
+
396
+ ```ts no-check
397
+ import { multipartBody, stepFetch, StepTransportError } from "@alexkroman1/aai/utils";
398
+
399
+ async function transcribeChunk(key: string, bytes: Uint8Array, index: number) {
400
+ "use step";
401
+
402
+ // Multipart as BYTES. Never a `FormData` — see below.
403
+ const part = multipartBody({
404
+ name: "audio",
405
+ filename: `chunk-${index}.wav`,
406
+ type: "audio/wav",
407
+ bytes,
408
+ });
409
+
410
+ const response = await stepFetch("https://sync.assemblyai.com/transcribe", {
411
+ method: "POST",
412
+ headers: { Authorization: key, ...part.headers },
413
+ body: part.body,
414
+ // Nothing here has a deadline of its own, and a hung request inside a step
415
+ // is a run that never finishes rather than one that retries.
416
+ signal: AbortSignal.timeout(60_000),
417
+ });
418
+ if (!response.ok) throw stepFailure(response);
419
+ return await response.json();
420
+ }
421
+ ```
422
+
423
+ **`fetch` speaks HTTP/2, and a fan-out is the worst case for that.** Node's
424
+ global `fetch` offers `h2` in ALPN and the far side decides; a server that takes
425
+ it gets every concurrent request from your process multiplexed onto ONE TCP
426
+ connection, sharing one flow-control window. That is fine for small JSON calls
427
+ and pathological for `mapInBatches` over large bodies. Measured on 8 concurrent
428
+ 17.66 MB uploads: `fetch` landed 14 of 16 at p50 8094ms, HTTP/1.1 landed 16 of
429
+ 16 at p50 3037ms.
430
+
431
+ **And the two it lost are the reason this matters more than the latency.** On
432
+ HTTP/2 a capacity limit arrives as a *stream reset* — `NGHTTP2_ENHANCE_YOUR_CALM`
433
+ — and a stream error carries no HTTP status, so `isTransientStatus` and
434
+ `retryAfter` cannot see it. Every sibling in the batch then retries in lockstep
435
+ into the same reset, exhausts `maxRetries`, and fails the run with
436
+ `TypeError: fetch failed`, whose real cause is two `cause` hops down where
437
+ nothing prints it. Over HTTP/1.1 the identical limit arrives as `503` with
438
+ `retry-after`, which your retry policy already reads.
439
+
440
+ Three rules come with it:
441
+
442
+ - **Bodies are BYTES or a string.** Never hand a `FormData`, `Blob`, `File`,
443
+ `Headers` or `Request` to a step's fetch: those are branded objects, checked
444
+ against the classes of whichever undici the fetch came from, and a foreign one
445
+ is silently stringified — `Content-Type: text/plain` with the 17-byte body
446
+ `[object FormData]`, answered `415`. `multipartBody()` is how a file becomes
447
+ bytes.
448
+ - **A connection failure is a `StepTransportError`**, distinct from a response
449
+ with a bad status because only the first is unclassifiable. It names its whole
450
+ `cause` chain, and `err.codes` is what to branch on (`ECONNRESET`,
451
+ `ETIMEDOUT`, …).
452
+ - **Test it with `stubStepFetch`** (`@alexkroman1/aai/testing`), not
453
+ `vi.stubGlobal("fetch", …)`. The global stub passes — an unpublished slot falls
454
+ back to it — while asserting a path production does not take, and it cannot see
455
+ the request body as bytes.
456
+
457
+ `stepGenerate` already goes through this, so a step that only calls a model gets
458
+ it for free.
459
+
460
+ ### A builtin's failure is its RESULT, so narrow it
461
+
462
+ `webSearch`, `visitWebpage` and `fetchJson` (`@alexkroman1/aai/tools`) answer
463
+ `T | ToolFailure` — they do not throw on an HTTP failure, a bot challenge or an
464
+ oversized body, because a tool usually wants to hand the model something useful
465
+ rather than fail the turn:
466
+
467
+ ```ts no-check
468
+ import { webSearch } from "@alexkroman1/aai/tools";
469
+ import { isToolFailure } from "@alexkroman1/aai/utils";
470
+
471
+ const found = await webSearch<{ results?: { url?: string }[] }>({ query, max_results: 4 });
472
+ // NOT `(found.results ?? [])` — a REFUSED search would then read as an empty web.
473
+ if (isToolFailure(found)) return `That search failed: ${found.error}`;
474
+ return (found.results ?? []).map((one) => one.url);
475
+ ```
476
+
477
+ **`?? []` is the mistake, and it is a quiet one.** Both shipped templates that
478
+ search wrote it, and one of them had a `catch` for this exact failure — which
479
+ never ran, because a `catch` cannot see a returned value. DuckDuckGo refuses
480
+ often enough that the empty answer is routine, and to the model "no results" and
481
+ "the search was blocked" are different facts: told the first, it concludes the
482
+ pages do not exist and tries again with different words until its budget is gone.
483
+
484
+ An UNTYPED call (`await fetchJson(url)`) stays loose and needs no narrowing —
485
+ naming a shape is what asks the compiler to make you handle the failure.
486
+
487
+ ### The page
488
+
489
+ A workflow app's `client.tsx` mounts with `page()` rather than `client()` —
490
+ there is no session to build, so no socket, no audio graph and no microphone
491
+ request. Everything else is the same file, React and Tailwind included.
492
+
493
+ ```tsx no-check
494
+ import { createWorkflowApi, page, useWorkflowRun } from "@alexkroman1/aai-ui";
495
+ import "@alexkroman1/aai-ui/styles.css";
496
+ import type { WorkflowOutputOf } from "@alexkroman1/aai";
497
+ import { useState } from "react";
498
+ import type { digest } from "./agent.ts";
499
+
500
+ // Hoisted: a client built in render is a new object every render.
501
+ const api = createWorkflowApi();
502
+
503
+ export function App() {
504
+ const [runId, setRunId] = useState<string>();
505
+ // The generic is what makes `run.output` typed rather than `unknown`.
506
+ const { run, polling } = useWorkflowRun<WorkflowOutputOf<typeof digest>>(runId, { api });
507
+
508
+ return (
509
+ <main>
510
+ <button
511
+ type="button"
512
+ onClick={async () => setRunId(await api.start("digest", { url: "https://example.com" }))}
513
+ >
514
+ Digest
515
+ </button>
516
+ {polling && <p>Working. You can close this tab — the run continues.</p>}
517
+ {run?.status === "completed" && <h2>{run.output.headline}</h2>}
518
+ </main>
519
+ );
520
+ }
521
+
522
+ page({ name: "Link Digest", component: App });
523
+ ```
524
+
525
+ `api.start()` resolves as soon as the RUN EXISTS, not when it finishes — that
526
+ is the whole mechanism. The `runId` is the entire client state, so it survives
527
+ a reload, a different device, or `curl`. Note the workflow is named by the key
528
+ it has in `workflows` above (`"digest"`); nothing else records that string, so
529
+ a rename there is a 400 here rather than a compile error.
530
+
531
+ The same routes are scriptable, which is the other half of having an API:
532
+
533
+ ```text
534
+ GET /workflows → the declared workflows, with input schemas
535
+ POST /workflows/runs → { runId } body: { workflow, input?, key?, wait? }
536
+ GET /workflows/runs/:id → a run snapshot
537
+ DELETE /workflows/runs/:id → cancel
538
+ GET /workflows/runs/:id/events → SSE
539
+ ```
540
+
198
541
  ## Pipeline mode
199
542
 
200
543
  Pipeline mode is the default: omitting `stt`/`llm`/`tts` (and `s2s`) gives
@@ -299,7 +642,7 @@ one-word backchannels like "yeah" don't cut it off);
299
642
  500 ms; `0` disables; interim transcripts only — committed turns always
300
643
  land). End-of-turn detection (how long a pause ends the user's turn)
301
644
  belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
302
- 2000 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
645
+ 1600 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
303
646
  pauses don't split a request.
304
647
  `deadAirCoverMs` is how long a turn may go silent before the transport speaks
305
648
  a short filler, so a long tool chain doesn't sound like a dropped call. It is
@@ -311,16 +654,20 @@ caller answers it and the answer barges in.
311
654
  a barge-in turns out to be noise — no user turn ever commits. The wait is not
312
655
  configurable: the resume fires once the transcript stream goes quiet with no
313
656
  final, so it can never race a real turn the STT is still endpointing.
314
- `preemptiveGeneration` (default `true`) starts generating the reply as soon
315
- as transcription is confident the caller has finished, and uses that
657
+ `preemptiveGeneration` (default **`false`**) starts generating the reply as
658
+ soon as transcription is confident the caller has finished, and uses that
316
659
  already-running answer if the committed transcript matches. It can shorten the
317
- pause before the agent speaks, and **how much is unmeasured** nobody has
318
- shown the size of the saving. What bounds the downside is that a speculation
319
- never speaks, calls a tool, or enters history until the real turn adopts it, so
320
- the worst case is a wasted request and a turn that behaves exactly as it would
321
- with the flag off. Set `preemptiveGeneration: false` to opt out worth doing
322
- on a tool-heavy agent, where a speculation that reaches a tool call is thrown
323
- away, so it is cost with no upside.
660
+ pause before the agent speaks, and it is off by default because the measurement
661
+ came back negative: over a tau2-bench retail run, 16 speculations started, 14
662
+ were adopted at a p50 head start of 0.44s, and 5 of those 14 (36%) were poisoned
663
+ after adoption by a tool call discarded whole, each having burned p50 0.69s
664
+ first. Net **+8ms per caller turn**, for 44% of its LLM requests thrown away.
665
+ What bounds the downside either way is that a speculation never speaks, calls a
666
+ tool, or enters history until the real turn adopts it, so the worst case is a
667
+ wasted request and a turn that behaves exactly as it would with the flag off.
668
+ Set `preemptiveGeneration: true` to opt in — worth trying on a text-heavy agent,
669
+ since 36% poisoned is a tool-calling agent's number, and pointless on a
670
+ tool-heavy one, where a speculation that reaches a tool call is thrown away.
324
671
 
325
672
  ## Providers
326
673
 
@@ -465,7 +812,8 @@ same way in `aai dev` and deployed.
465
812
 
466
813
  ```ts no-check
467
814
  ctx.env: Readonly<Record<string, string>> // secrets from .env / aai secret put
468
- ctx.state: S // per-session mutable state (agent's `state` factory)
815
+ ctx.slots: SlotStore // where sessionSlot() keeps this session's state
816
+ // reach for the slot, never this (see "Session state")
469
817
  ctx.db: Db // SQL database, needs storage enabled (see Database section)
470
818
  ctx.messages: readonly Message[] // conversation history [{role, content}]
471
819
  ctx.sessionId: string // unique session ID
@@ -495,17 +843,9 @@ export const lookup = tool({
495
843
  });
496
844
  ```
497
845
 
498
- **Typing `ctx.state` is optional.** `ctx.state` is untyped by default, and
499
- the project's tsconfig turns off `noImplicitAny`, so both of these compile
500
- with no annotations and no errors:
501
-
502
- ```ts no-check
503
- ctx.state.count++;
504
- ctx.state.incidents.filter((i) => i.status === "open");
505
- ```
506
-
507
- Write the code first. Do NOT add type annotations defensively — almost
508
- nothing requires them, and time spent on them is time not spent on the agent.
846
+ **The project's tsconfig turns off `noImplicitAny`, so write the code first.**
847
+ Do NOT add type annotations defensively almost nothing requires them, and time
848
+ spent on them is time not spent on the agent.
509
849
 
510
850
  **The one exception, and it is not optional: annotate any variable you
511
851
  declare empty.** With `noImplicitAny` off, TypeScript does not widen an empty
@@ -527,35 +867,74 @@ so the next push reports the next line, and you can burn a whole session
527
867
  fixing one call site at a time. This is the single most common way a
528
868
  generated agent fails to build.
529
869
 
530
- Declaring a state type is still worth it once the shape is settled, because
531
- it turns a misspelled field into a compile error instead of `undefined` at
532
- runtime:
870
+ ### Session state
533
871
 
534
- ```ts
535
- import { agent, tool } from "@alexkroman1/aai";
536
- import type { ToolContext } from "@alexkroman1/aai"; // types need `import type`
537
- import { z } from "zod";
872
+ **A `sessionSlot` is the only way to keep state across a session's tool calls**,
873
+ and it is one declaration in a shared module:
874
+
875
+ ```ts no-check
876
+ // shared.ts — the one place the shape is written down.
877
+ import { sessionSlot } from "@alexkroman1/aai";
878
+
879
+ export type Incident = { id: string; status: "open" | "closed" };
538
880
 
539
- type Incident = { id: string; status: "open" | "closed" };
540
- type State = { incidents: Incident[] };
881
+ export const incidentSlot = sessionSlot("incidents", () => ({ items: [] as Incident[] }));
882
+ ```
883
+
884
+ ```ts no-check
885
+ // tools/list_open.ts — `slot.tool` READS: the body is handed the value, typed.
886
+ import { incidentSlot } from "../shared.ts";
541
887
 
542
- const listOpen = tool({
888
+ export default incidentSlot.tool({
543
889
  description: "List open incidents",
544
- execute: (_args, ctx: ToolContext<State>) => {
545
- // `i` infers as Incident, and `i.staus` would now be an error.
546
- return ctx.state.incidents.filter((i) => i.status === "open");
547
- },
890
+ // `i` infers as Incident, and `i.staus` would now be an error.
891
+ execute: (_args, incidents) => incidents.items.filter((i) => i.status === "open"),
548
892
  });
893
+ ```
549
894
 
550
- export default agent({
551
- name: "Dispatch",
552
- state: (): State => ({ incidents: [] }),
553
- tools: { listOpen },
895
+ ```ts no-check
896
+ // tools/open_incident.ts — `slot.updateTool` WRITES: mutate what you are handed.
897
+ import { incidentSlot } from "../shared.ts";
898
+ import { z } from "zod";
899
+
900
+ export default incidentSlot.updateTool({
901
+ description: "Open an incident",
902
+ inputSchema: z.object({ id: z.string() }),
903
+ execute: ({ id }, incidents) => {
904
+ incidents.items.push({ id, status: "open" });
905
+ return { open: incidents.items.length };
906
+ },
554
907
  });
555
908
  ```
556
909
 
557
- A tool annotated with a state shape the agent's factory doesn't produce is a
558
- compile error, which is the point.
910
+ Four rules, and each is an error rather than advice if you get it wrong:
911
+
912
+ - **`tool` reads, `updateTool` writes.** What a read is handed is FROZEN, so
913
+ mutating it throws instead of quietly going nowhere.
914
+ - **A write is SYNCHRONOUS.** The value you mutate is stored the moment your body
915
+ returns, so an `updateTool` body may not `await`. When you need a model call or
916
+ a fetch first, do it in an ordinary `tool()` and then mutate:
917
+
918
+ ```ts no-check
919
+ execute: async (args, ctx) => {
920
+ const priced = await ctx.generate({ prompt: `price ${args.sku}` });
921
+ return cartSlot.update(ctx, (cart) => {
922
+ cart.total = Number(priced.text);
923
+ return { total: cart.total };
924
+ });
925
+ }
926
+ ```
927
+
928
+ - **Hold plain data.** Objects, arrays, strings, numbers, booleans and null. A
929
+ `Map`, a `Set`, a `Date` or a class instance is refused with the field named,
930
+ because none of them survives being stored.
931
+ - **State is STORED when your app has a database** (`aai storage enable`, or a
932
+ `DATABASE_URL` in `.env`), so a crash or a redeploy no longer loses it. Without
933
+ one it lives in memory for the life of the process. You write the same code
934
+ either way; that is the reason for the rules above.
935
+
936
+ There is nothing to declare on `agent()` — the slot owns its own default. Use
937
+ `syncState: slot.projection(view)` to show state to a custom client.
559
938
 
560
939
  **`verbatimModuleSyntax` applies to every type you import** — `ToolContext`,
561
940
  `ToolDef`, `Message`, provider types. A plain
@@ -570,31 +949,30 @@ that's also how S2S agents use it. Pass a Zod schema as `schema` for typed
570
949
  structured output (`generateObject`-style): the result's `object` carries
571
950
  the parsed, typed value. A plain JSON Schema object also works.
572
951
 
573
- ### Inline tool example
952
+ ### A tool that calls an API
574
953
 
575
954
  ```ts
576
- import { agent, tool } from "@alexkroman1/aai";
955
+ // tools/get_weather.ts → the model calls this "get_weather"
956
+ import { tool } from "@alexkroman1/aai";
577
957
  import { z } from "zod";
578
958
 
579
- export default agent({
580
- name: "Weather Agent",
581
- tools: {
582
- get_weather: tool({
583
- description: "Get current weather for a city",
584
- inputSchema: z.object({
585
- city: z.string().describe("City name"),
586
- }),
587
- async execute({ city }, ctx) {
588
- const resp = await fetch(
589
- `https://api.example.com/weather?q=${city}&key=${ctx.env.WEATHER_KEY}`,
590
- );
591
- return resp.json();
592
- },
593
- }),
959
+ export default tool({
960
+ description: "Get current weather for a city",
961
+ inputSchema: z.object({
962
+ city: z.string().describe("City name"),
963
+ }),
964
+ async execute({ city }, ctx) {
965
+ const resp = await fetch(
966
+ `https://api.example.com/weather?q=${city}&key=${ctx.env.WEATHER_KEY}`,
967
+ );
968
+ return resp.json();
594
969
  },
595
970
  });
596
971
  ```
597
972
 
973
+ Nothing else. `agent.ts` does not import it, does not list it, and takes no
974
+ `tools` field at all — see "A file in `tools/` IS a tool" below.
975
+
598
976
  **Calling the network builtins from your own tool code.** `web_search`,
599
977
  `visit_webpage` and `fetch_json` are declared to the MODEL — the LLM calls
600
978
  them, and they are not on `ctx`. When your own `execute` needs one, import
@@ -650,16 +1028,18 @@ the tool also returns an error, because `Promise<DrugInfo>` does not accept
650
1028
  `{ error: "not found" }`. Every such annotation eventually costs a build
651
1029
  round to widen into a union. Let it infer.
652
1030
 
653
- ### Separate file pattern
1031
+ ### A file in `tools/` IS a tool — there is no registration step
654
1032
 
655
- For complex tools — `tools/` is a convention, any import path works:
1033
+ **`tools/` is not a convention, it is the mechanism.** A file there is named for
1034
+ the tool the model calls, default-exports it, and is picked up by the build. It
1035
+ is not imported by `agent.ts` and not listed anywhere:
656
1036
 
657
1037
  ```ts
658
- // tools/roll_dice.ts
1038
+ // tools/roll_dice.ts → the model calls this "roll_dice"
659
1039
  import { tool } from "@alexkroman1/aai";
660
1040
  import { z } from "zod";
661
1041
 
662
- export const rollDice = tool({
1042
+ export default tool({
663
1043
  description: "Roll dice",
664
1044
  inputSchema: z.object({ sides: z.number() }),
665
1045
  execute({ sides }) {
@@ -669,16 +1049,42 @@ export const rollDice = tool({
669
1049
  ```
670
1050
 
671
1051
  ```ts no-check
672
- // agent.ts
1052
+ // agent.ts — nothing about tools appears here
673
1053
  import { agent } from "@alexkroman1/aai";
674
- import { rollDice } from "./tools/roll_dice.ts";
675
1054
 
676
- export default agent({
677
- name: "Dice Agent",
678
- tools: { roll_dice: rollDice },
679
- });
1055
+ export default agent({ name: "Dice Agent" });
1056
+ ```
1057
+
1058
+ Three rules come with it, each a build error naming the file:
1059
+
1060
+ - **The file name is the tool name**, so it must be lowercase, start with a
1061
+ letter, and join words with `_` — `tools/incident_create.ts`, never
1062
+ `incident-create.ts`. Renaming the file renames the tool.
1063
+ - **The export is the DEFAULT export**, and it must be a `tool()` (or a
1064
+ `slot.tool()` / `slot.updateTool()`). A file exporting something else is
1065
+ named at build time rather than becoming a tool that fails per turn.
1066
+ - **`tools/` is flat.** A nested file is rejected, because a provider will not
1067
+ accept a tool name with a `/` in it and inventing a flattening rule would
1068
+ freeze a guess. This applies to a nested HELPER too, not just a nested tool —
1069
+ the build cannot tell them apart, so put shared helpers beside `agent.ts`
1070
+ rather than under `tools/`. The error names the file and both ways out.
1071
+
1072
+ A tool that closes over module-local state, or one built by your own wrapper,
1073
+ still gets its own file — the file names the instance and the factory lives
1074
+ beside it:
1075
+
1076
+ ```ts no-check
1077
+ // tools/to_hotel_assistant.ts
1078
+ import { delegationTool } from "../routing.ts";
1079
+
1080
+ export default delegationTool("hotel");
680
1081
  ```
681
1082
 
1083
+ Why discovery rather than a map: the map was 62 lines across the shipped
1084
+ templates whose entire content was `snake_case_name: camelCaseImport`, and
1085
+ forgetting one line was **silent** — the file compiled, every check passed, and
1086
+ the tool simply never reached the model.
1087
+
682
1088
  ## Built-in tools
683
1089
 
684
1090
  Enable via `builtinTools` in `agent()`. **When `builtinTools` is omitted, none
@@ -770,7 +1176,7 @@ const rows = await ctx.db.query<{ value: { name: string } }>(
770
1176
 
771
1177
  Use `ctx.db` for data that must outlive the session (saves, filed records,
772
1178
  user profiles). For scratch that only the current session needs, prefer
773
- `ctx.state` (per-session mutable state no storage required); the
1179
+ a `sessionSlot` (per-session state, stored when the app has a database); the
774
1180
  `remember`/`recall` builtins likewise remain for session-scoped notes the
775
1181
  LLM manages itself.
776
1182
 
@@ -907,11 +1313,12 @@ also what you want for anything that can be a string, an array, or null.
907
1313
  **`useAgentState`** — the agent's session state, pushed automatically:
908
1314
 
909
1315
  ```ts no-check
1316
+ // shared.ts — the slot owns the shape; `agent()` has no `state` field.
1317
+ export const cartSlot = sessionSlot("cart", () => ({ cart: [] as Item[], staffPin: "" }));
1318
+
910
1319
  // agent.ts
911
1320
  export default agent({
912
- state: () => ({ cart: [] as Item[], staffPin: "" }),
913
- syncState: (s) => ({ cart: s.cart }), // staffPin never leaves the server
914
- tools: { ... },
1321
+ syncState: cartSlot.projection((s) => ({ cart: s.cart })), // staffPin stays server-side
915
1322
  });
916
1323
 
917
1324
  // client.tsx
@@ -1085,7 +1492,7 @@ Common mistakes when working in aai projects:
1085
1492
  preview redeploys itself, production on the next publish.
1086
1493
  - **The database is per-app.** Rows are shared by every session of one
1087
1494
  deployment — key them yourself if sessions must not see each other's data
1088
- (or keep session-scoped data in `ctx.state`).
1495
+ (or keep session-scoped data in a `sessionSlot`).
1089
1496
  - **Rime language codes are ISO 639-3** (3-letter, e.g. `"eng"`), not
1090
1497
  ISO 639-1 (`"en"`).
1091
1498