experimental-ash 0.41.0 → 0.43.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 (158) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/docs/internals/hooks.md +53 -11
  3. package/dist/docs/public/README.md +8 -8
  4. package/dist/docs/public/advanced/{auth-and-route-protection.md → auth-and-route-protection.mdx} +11 -0
  5. package/dist/docs/public/advanced/context-control.md +4 -4
  6. package/dist/docs/public/advanced/{evals.md → evals.mdx} +11 -1
  7. package/dist/docs/public/advanced/{hooks.md → hooks.mdx} +37 -46
  8. package/dist/docs/public/advanced/instrumentation.md +92 -3
  9. package/dist/docs/public/advanced/project-layout.md +5 -5
  10. package/dist/docs/public/advanced/runs-and-streaming.md +8 -2
  11. package/dist/docs/public/advanced/session-context.md +1 -1
  12. package/dist/docs/public/advanced/typescript-api.md +49 -6
  13. package/dist/docs/public/advanced/vercel-deployment.md +1 -1
  14. package/dist/docs/public/agent-ts.md +5 -5
  15. package/dist/docs/public/channels/{discord.md → discord.mdx} +11 -0
  16. package/dist/docs/public/channels/index.md +10 -10
  17. package/dist/docs/public/channels/{slack.md → slack.mdx} +11 -0
  18. package/dist/docs/public/channels/{teams.md → teams.mdx} +12 -0
  19. package/dist/docs/public/channels/{telegram.md → telegram.mdx} +11 -0
  20. package/dist/docs/public/channels/{twilio.md → twilio.mdx} +11 -0
  21. package/dist/docs/public/{connections.md → connections.mdx} +18 -6
  22. package/dist/docs/public/frontend/README.md +16 -0
  23. package/dist/docs/public/frontend/meta.json +3 -0
  24. package/dist/docs/public/frontend/nextjs.md +192 -0
  25. package/dist/docs/public/frontend/use-ash-agent.md +332 -0
  26. package/dist/docs/public/{getting-started.md → getting-started.mdx} +12 -1
  27. package/dist/docs/public/{human-in-the-loop.md → human-in-the-loop.mdx} +12 -1
  28. package/dist/docs/public/meta.json +1 -0
  29. package/dist/docs/public/sandbox.md +1 -1
  30. package/dist/docs/public/{schedules.md → schedules.mdx} +9 -0
  31. package/dist/docs/public/skills.md +2 -2
  32. package/dist/docs/public/{subagents.md → subagents.mdx} +10 -0
  33. package/dist/docs/public/{tools.md → tools.mdx} +62 -36
  34. package/dist/src/channel/adapter.d.ts +13 -0
  35. package/dist/src/channel/instrumentation.d.ts +10 -0
  36. package/dist/src/channel/instrumentation.js +1 -0
  37. package/dist/src/channel/send.js +1 -1
  38. package/dist/src/channel/types.d.ts +16 -0
  39. package/dist/src/cli/commands/channels.d.ts +2 -1
  40. package/dist/src/cli/commands/channels.js +1 -1
  41. package/dist/src/compiler/manifest.d.ts +13 -1
  42. package/dist/src/compiler/manifest.js +1 -1
  43. package/dist/src/compiler/module-map.js +1 -1
  44. package/dist/src/compiler/normalize-manifest.js +1 -1
  45. package/dist/src/compiler/normalize-skill.d.ts +15 -2
  46. package/dist/src/compiler/normalize-skill.js +1 -1
  47. package/dist/src/compiler/normalize-tool.js +1 -1
  48. package/dist/src/context/dynamic-skill-lifecycle.d.ts +23 -0
  49. package/dist/src/context/dynamic-skill-lifecycle.js +1 -0
  50. package/dist/src/context/dynamic-tool-lifecycle.d.ts +19 -23
  51. package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
  52. package/dist/src/context/hook-lifecycle.d.ts +4 -6
  53. package/dist/src/context/hook-lifecycle.js +1 -1
  54. package/dist/src/context/keys.d.ts +10 -9
  55. package/dist/src/context/keys.js +1 -1
  56. package/dist/src/context/providers/connection.d.ts +9 -0
  57. package/dist/src/context/providers/connection.js +1 -1
  58. package/dist/src/context/providers/sandbox.js +1 -1
  59. package/dist/src/execution/ash-workflow-attributes.d.ts +118 -0
  60. package/dist/src/execution/ash-workflow-attributes.js +1 -0
  61. package/dist/src/execution/channel-context.d.ts +5 -0
  62. package/dist/src/execution/channel-context.js +1 -0
  63. package/dist/src/execution/create-session-step.d.ts +28 -1
  64. package/dist/src/execution/create-session-step.js +1 -1
  65. package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
  66. package/dist/src/execution/durable-session-store.d.ts +7 -0
  67. package/dist/src/execution/node-step.d.ts +2 -2
  68. package/dist/src/execution/node-step.js +1 -1
  69. package/dist/src/execution/runtime-context.js +1 -1
  70. package/dist/src/execution/sandbox/prewarm.js +1 -1
  71. package/dist/src/execution/session.d.ts +6 -0
  72. package/dist/src/execution/session.js +2 -2
  73. package/dist/src/execution/skills/instructions.d.ts +3 -2
  74. package/dist/src/execution/subagent-tool.js +1 -1
  75. package/dist/src/execution/workflow-entry.js +1 -1
  76. package/dist/src/execution/workflow-steps.js +1 -1
  77. package/dist/src/harness/attachment-staging.js +1 -1
  78. package/dist/src/harness/code-mode.d.ts +0 -5
  79. package/dist/src/harness/code-mode.js +1 -1
  80. package/dist/src/harness/emission.d.ts +1 -1
  81. package/dist/src/harness/emission.js +1 -1
  82. package/dist/src/harness/instrumentation-config.d.ts +1 -1
  83. package/dist/src/harness/instrumentation-metadata.d.ts +23 -0
  84. package/dist/src/harness/instrumentation-metadata.js +1 -0
  85. package/dist/src/harness/otel-integration.d.ts +2 -2
  86. package/dist/src/harness/otel-integration.js +1 -1
  87. package/dist/src/harness/step-hooks.js +1 -1
  88. package/dist/src/harness/tool-loop.js +1 -1
  89. package/dist/src/harness/turn-tag-state.d.ts +50 -0
  90. package/dist/src/harness/turn-tag-state.js +1 -0
  91. package/dist/src/harness/types.d.ts +20 -2
  92. package/dist/src/internal/application/package.js +1 -1
  93. package/dist/src/internal/authored-definition/schema-backed.d.ts +0 -1
  94. package/dist/src/internal/authored-definition/schema-backed.js +1 -1
  95. package/dist/src/internal/instrumentation.d.ts +39 -0
  96. package/dist/src/internal/instrumentation.js +1 -0
  97. package/dist/src/internal/workflow/builtins.d.ts +32 -0
  98. package/dist/src/internal/workflow/builtins.js +1 -1
  99. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.d.ts +1 -1
  100. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.js +1 -1
  101. package/dist/src/internal/workflow-bundle/workflow-core-shim.d.ts +34 -0
  102. package/dist/src/internal/workflow-bundle/workflow-core-shim.js +1 -1
  103. package/dist/src/internal/workflow-bundle/workflow-transformer.js +1 -1
  104. package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
  105. package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -2
  106. package/dist/src/public/channels/slack/attachments.js +1 -1
  107. package/dist/src/public/channels/slack/index.d.ts +1 -1
  108. package/dist/src/public/channels/slack/slackChannel.d.ts +6 -0
  109. package/dist/src/public/channels/slack/slackChannel.js +1 -1
  110. package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
  111. package/dist/src/public/channels/twilio/index.d.ts +1 -1
  112. package/dist/src/public/channels/twilio/twilioChannel.d.ts +6 -0
  113. package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
  114. package/dist/src/public/definitions/defineChannel.d.ts +12 -2
  115. package/dist/src/public/definitions/defineChannel.js +1 -1
  116. package/dist/src/public/definitions/hook.d.ts +3 -11
  117. package/dist/src/public/definitions/instrumentation.d.ts +88 -2
  118. package/dist/src/public/definitions/skill.d.ts +5 -0
  119. package/dist/src/public/definitions/tool.d.ts +25 -66
  120. package/dist/src/public/definitions/tool.js +1 -1
  121. package/dist/src/public/instrumentation/index.d.ts +1 -4
  122. package/dist/src/public/instrumentation/index.js +1 -1
  123. package/dist/src/public/skills/index.d.ts +2 -0
  124. package/dist/src/public/skills/index.js +1 -1
  125. package/dist/src/public/tools/index.d.ts +2 -2
  126. package/dist/src/public/tools/index.js +1 -1
  127. package/dist/src/runtime/agent/mock-model-adapter.js +4 -7
  128. package/dist/src/runtime/agent/mock-model-skill-selection.d.ts +9 -0
  129. package/dist/src/runtime/agent/mock-model-skill-selection.js +4 -0
  130. package/dist/src/runtime/attributes/emit.d.ts +73 -0
  131. package/dist/src/runtime/attributes/emit.js +1 -0
  132. package/dist/src/runtime/channels/registry.js +1 -1
  133. package/dist/src/runtime/connections/mcp-client.js +1 -1
  134. package/dist/src/runtime/framework-tools/code-mode-connection-auth.d.ts +2 -0
  135. package/dist/src/runtime/framework-tools/connection-search-dynamic.d.ts +34 -0
  136. package/dist/src/runtime/framework-tools/connection-search-dynamic.js +1 -0
  137. package/dist/src/runtime/framework-tools/index.d.ts +7 -5
  138. package/dist/src/runtime/framework-tools/index.js +1 -1
  139. package/dist/src/runtime/prompt/connections.js +1 -1
  140. package/dist/src/runtime/resolve-agent-graph.js +1 -1
  141. package/dist/src/runtime/resolve-agent.js +1 -1
  142. package/dist/src/runtime/resolve-dynamic-skill.d.ts +8 -0
  143. package/dist/src/runtime/resolve-dynamic-skill.js +1 -0
  144. package/dist/src/runtime/resolve-dynamic-tool.js +1 -1
  145. package/dist/src/runtime/sessions/compiled-agent-cache.js +1 -1
  146. package/dist/src/runtime/sessions/runtime-context-keys.js +1 -1
  147. package/dist/src/runtime/types.d.ts +13 -4
  148. package/dist/src/shared/dynamic-tool-definition.d.ts +57 -80
  149. package/dist/src/shared/dynamic-tool-definition.js +1 -1
  150. package/dist/src/shared/guards.d.ts +14 -0
  151. package/dist/src/shared/guards.js +1 -1
  152. package/dist/src/shared/skill-definition.d.ts +5 -4
  153. package/dist/src/shared/tool-definition.d.ts +12 -0
  154. package/package.json +2 -1
  155. package/dist/src/runtime/framework-tools/connection-search.d.ts +0 -57
  156. package/dist/src/runtime/framework-tools/connection-search.js +0 -1
  157. package/dist/src/runtime/framework-tools/connection-tools.d.ts +0 -55
  158. package/dist/src/runtime/framework-tools/connection-tools.js +0 -1
@@ -0,0 +1,332 @@
1
+ ---
2
+ title: "useAshAgent"
3
+ description: "React hook for building chat and agent UIs against an Ash agent."
4
+ ---
5
+
6
+ `useAshAgent()` is the easiest way to put an Ash agent behind a React UI.
7
+
8
+ It gives a component everything it needs to feel responsive:
9
+
10
+ - **One send call** — text, attachments, and human-in-the-loop responses go through a single API
11
+ - **Live streaming** — assistant text, reasoning, tool calls, and tool results stream as they happen
12
+ - **Composer state** — `ready`, `submitted`, `streaming`, `error`, plus `stop()` and `reset()` built in
13
+ - **AI SDK compatible** — `messages` are `UIMessage[]`, so they drop straight into AI SDK UI primitives
14
+ - **Resumable sessions** — persist a single cursor and pick the conversation back up after reload
15
+
16
+ ## Basic Usage
17
+
18
+ ```tsx
19
+ "use client";
20
+
21
+ import { useAshAgent } from "experimental-ash/react";
22
+
23
+ export function Chat() {
24
+ const agent = useAshAgent();
25
+ const isBusy = agent.status === "submitted" || agent.status === "streaming";
26
+
27
+ return (
28
+ <form
29
+ onSubmit={(event) => {
30
+ event.preventDefault();
31
+ const form = new FormData(event.currentTarget);
32
+ const message = String(form.get("message") ?? "").trim();
33
+ if (message.length > 0) {
34
+ void agent.sendMessage(message);
35
+ }
36
+ }}
37
+ >
38
+ {agent.data.messages.map((message) => (
39
+ <article key={message.id}>
40
+ <header>{message.role}</header>
41
+ {message.parts.map((part, index) =>
42
+ part.type === "text" ? <p key={index}>{part.text}</p> : null,
43
+ )}
44
+ </article>
45
+ ))}
46
+ <input name="message" disabled={isBusy} />
47
+ <button disabled={isBusy} type="submit">
48
+ Send
49
+ </button>
50
+ </form>
51
+ );
52
+ }
53
+ ```
54
+
55
+ By default, the hook targets same-origin Ash routes such as
56
+ `/ash/v1/session`. In Next.js apps, pair it with [`withAsh()`](./nextjs.md) so
57
+ your component can call the agent without configuring a separate URL.
58
+
59
+ ## Returned State
60
+
61
+ `useAshAgent()` returns the current UI state plus commands:
62
+
63
+ | Field | What it is |
64
+ | ------------- | ------------------------------------------------------------------------------------------------ |
65
+ | `data` | Projected UI state from the reducer. Defaults to `{ messages }`. |
66
+ | `status` | `"ready"`, `"submitted"`, `"streaming"`, or `"error"`. Drives the composer. |
67
+ | `error` | The last `Error` thrown, if any. |
68
+ | `events` | Raw Ash stream events for this session — see [Sessions And Streaming](../runs-and-streaming.md). |
69
+ | `session` | Serializable session cursor (`sessionId`, `continuationToken`, `streamIndex`). |
70
+ | `sendMessage` | Send plain text. |
71
+ | `send` | Send the full turn payload (multi-part messages, HITL responses). |
72
+ | `stop` | Abort the active request. |
73
+ | `reset` | Clear local events, data, errors, and the local session cursor. |
74
+
75
+ Most chat UIs only need `data.messages` and `status`. Reach for `events` when
76
+ you want to render lower-level activity (tool calls, reasoning deltas) the
77
+ default reducer doesn't surface.
78
+
79
+ ## Render Messages
80
+
81
+ By default, `useAshAgent()` returns chat-style message data:
82
+
83
+ ```ts
84
+ type AshMessageData = {
85
+ readonly messages: readonly AshMessage[];
86
+ };
87
+ ```
88
+
89
+ `AshMessage` follows the AI SDK `UIMessage` convention, so `messages` drops
90
+ into any AI SDK UI primitive that accepts a `UIMessage[]`. Parts include user
91
+ text, assistant text, reasoning, tool calls, tool results, and input requests.
92
+
93
+ To project events into a different shape, pass your own
94
+ [reducer](#custom-reducers).
95
+
96
+ ## Send A Message
97
+
98
+ Use `sendMessage()` for plain text:
99
+
100
+ ```tsx
101
+ await agent.sendMessage("Summarize this session.");
102
+ ```
103
+
104
+ Use `send()` when you need the full turn payload:
105
+
106
+ ```tsx
107
+ await agent.send({
108
+ message: [
109
+ { type: "text", text: "What is in this file?" },
110
+ {
111
+ type: "file",
112
+ // base64 data URL — e.g. "data:application/pdf;base64,JVBERi0..."
113
+ data: fileDataUrl,
114
+ mediaType: "application/pdf",
115
+ filename: "report.pdf",
116
+ },
117
+ ],
118
+ });
119
+ ```
120
+
121
+ ## Human-In-The-Loop Responses
122
+
123
+ When the stream asks for input, send responses back through the same session:
124
+
125
+ ```tsx
126
+ await agent.send({
127
+ inputResponses: [
128
+ {
129
+ requestId,
130
+ optionId: "approve",
131
+ },
132
+ ],
133
+ });
134
+ ```
135
+
136
+ The default reducer marks the matching tool part as responded immediately, then
137
+ updates it again when Ash streams the resumed result.
138
+
139
+ ## Stop Or Reset
140
+
141
+ `stop()` aborts the active request:
142
+
143
+ ```tsx
144
+ agent.stop();
145
+ ```
146
+
147
+ `reset()` clears local events, data, errors, and the local session cursor:
148
+
149
+ ```tsx
150
+ agent.reset();
151
+ ```
152
+
153
+ Use `reset()` when you want the next `sendMessage()` call to start a fresh
154
+ durable session.
155
+
156
+ ## Persist Session State
157
+
158
+ Persist `session` when you want to resume a browser conversation after reload:
159
+
160
+ ```tsx
161
+ const [initialSession] = useState(() => {
162
+ const raw = localStorage.getItem("ash-session");
163
+ return raw ? JSON.parse(raw) : undefined;
164
+ });
165
+
166
+ const agent = useAshAgent({
167
+ initialSession,
168
+ onSessionChange(session) {
169
+ localStorage.setItem("ash-session", JSON.stringify(session));
170
+ },
171
+ });
172
+ ```
173
+
174
+ The session snapshot contains the `sessionId`, `continuationToken`, and
175
+ `streamIndex`. Store the full object, not just one field.
176
+
177
+ ## Add Per-Turn Client Context
178
+
179
+ Use `prepareSend` to attach fresh browser state before each request:
180
+
181
+ ```tsx
182
+ const agent = useAshAgent({
183
+ prepareSend(input) {
184
+ return {
185
+ ...input,
186
+ clientContext: {
187
+ pathname: window.location.pathname,
188
+ // any other page-scoped state you want the model to see
189
+ selectedRepository,
190
+ },
191
+ };
192
+ },
193
+ });
194
+ ```
195
+
196
+ `clientContext` is rendered as ephemeral model context for the next turn only.
197
+ It is never persisted to durable message history.
198
+
199
+ Use authored lifecycle hooks for server-side enrichment:
200
+
201
+ ```ts
202
+ // agent/hooks/profile.ts
203
+ import { defineHook } from "experimental-ash/hooks";
204
+
205
+ export default defineHook({
206
+ lifecycle: {
207
+ async session(_input, ctx) {
208
+ return {
209
+ modelContext: [
210
+ {
211
+ role: "system",
212
+ content: `Session ${ctx.session.sessionId} has server-side profile context.`,
213
+ },
214
+ ],
215
+ };
216
+ },
217
+ },
218
+ });
219
+ ```
220
+
221
+ The useful split is:
222
+
223
+ - React `prepareSend`: page state from the browser
224
+ - Ash `agent/hooks/*.ts`: runtime state, durable context, policy, and dynamic
225
+ skills
226
+
227
+ ## Custom Hosts And Headers
228
+
229
+ Pass `host` when the Ash server is not same-origin:
230
+
231
+ ```tsx
232
+ const agent = useAshAgent({
233
+ host: "https://agent.example.com",
234
+ });
235
+ ```
236
+
237
+ Pass `auth` or `headers` when the Ash channel requires credentials:
238
+
239
+ ```tsx
240
+ const agent = useAshAgent({
241
+ auth: {
242
+ bearer: async () => await getAccessToken(),
243
+ },
244
+ });
245
+ ```
246
+
247
+ Function-valued credentials are resolved before every HTTP request.
248
+
249
+ ## Custom Reducers
250
+
251
+ Write a reducer when you want your own UI shape derived from the raw event
252
+ stream — the default chat projection isn't the right fit. For example, an
253
+ agent dashboard that tracks every tool call as it happens:
254
+
255
+ ```tsx
256
+ type ToolActivity = {
257
+ readonly callId: string;
258
+ readonly toolName: string;
259
+ readonly status: "running" | "completed" | "failed";
260
+ };
261
+
262
+ const agent = useAshAgent({
263
+ reducer: {
264
+ initial() {
265
+ return { tools: [] as readonly ToolActivity[] };
266
+ },
267
+ reduce(data, event) {
268
+ if (event.type === "actions.requested") {
269
+ const started: ToolActivity[] = event.data.actions
270
+ .filter((action) => action.kind === "tool-call")
271
+ .map((action) => ({
272
+ callId: action.callId,
273
+ toolName: action.toolName,
274
+ status: "running",
275
+ }));
276
+ return { tools: [...data.tools, ...started] };
277
+ }
278
+ if (event.type === "action.result" && event.data.result.kind === "tool-result") {
279
+ const { callId } = event.data.result;
280
+ return {
281
+ tools: data.tools.map((tool) =>
282
+ tool.callId === callId ? { ...tool, status: event.data.status } : tool,
283
+ ),
284
+ };
285
+ }
286
+ return data;
287
+ },
288
+ },
289
+ });
290
+ ```
291
+
292
+ `agent.data.tools` now drives the UI. The reducer sees the full Ash event
293
+ stream — see [Sessions And Streaming](../runs-and-streaming.md) — plus
294
+ client-side projection events (`client.message.submitted`,
295
+ `client.message.failed`, `client.input.responded`) for optimistic updates.
296
+
297
+ ## Lifecycle Callbacks
298
+
299
+ Use callbacks for side effects:
300
+
301
+ ```tsx
302
+ const agent = useAshAgent({
303
+ onEvent(event) {
304
+ console.debug("ash event", event.type);
305
+ },
306
+ onError(error) {
307
+ reportError(error);
308
+ },
309
+ onFinish(snapshot) {
310
+ console.debug("turn finished", snapshot.status);
311
+ },
312
+ });
313
+ ```
314
+
315
+ ## When Options Change
316
+
317
+ `host`, `auth`, `headers`, `initialSession`, `reducer`, and
318
+ `maxReconnectAttempts` are read once when the hook creates its internal store.
319
+ Remount the component to switch them.
320
+
321
+ Callback functions (`onEvent`, `onError`, `onFinish`, `onSessionChange`,
322
+ `prepareSend`) are refreshed on every render.
323
+
324
+ To change credentials without remounting, pass a function to `auth` or
325
+ `headers` — the underlying client invokes it before every HTTP request.
326
+
327
+ ## What To Read Next
328
+
329
+ - [Next.js](./nextjs.md)
330
+ - [Sessions And Streaming](../runs-and-streaming.md)
331
+ - [Human In The Loop](../human-in-the-loop.md)
332
+ - [Hooks](../hooks.md)
@@ -6,6 +6,15 @@ description: "Install, create your first agent, and run it locally."
6
6
  This guide gets a small Ash app running locally and shows the current request loop: build, run,
7
7
  message, stream, and follow up.
8
8
 
9
+ <CopyPrompt text="Set up an Ash app, a filesystem-first framework for durable agents. In the user's project, scaffold a new Ash app with pnpm create experimental-ash-agent if no app exists, or add experimental-ash to an existing app. Ensure agent/instructions.md and agent/agent.ts exist, add a first typed weather tool under agent/tools/get_weather.ts, run the local dev workflow, create a session, attach to the stream, and send a follow-up. Adapt model and provider choices to the project, and do not commit unless the user asks.">
10
+ Set up an Ash app, a filesystem-first framework for durable agents. In the user's project,
11
+ scaffold a new Ash app with pnpm create experimental-ash-agent if no app exists, or add
12
+ experimental-ash to an existing app. Ensure agent/instructions.md and agent/agent.ts exist, add a
13
+ first typed weather tool under agent/tools/get_weather.ts, run the local dev workflow, create a
14
+ session, attach to the stream, and send a follow-up. Adapt model and provider choices to the
15
+ project, and do not commit unless the user asks.
16
+ </CopyPrompt>
17
+
9
18
  ## Prerequisites
10
19
 
11
20
  - Node `24.x`
@@ -158,8 +167,10 @@ curl -X POST http://127.0.0.1:3000/ash/v1/session/<sessionId> \
158
167
 
159
168
  ## What To Read Next
160
169
 
170
+ - [Next.js](./frontend/nextjs.md) for running Ash behind a Next.js app
171
+ - [`useAshAgent`](./frontend/use-ash-agent.md) for building browser chat and agent UIs
161
172
  - [Project Layout](./project-layout.md) for every supported authored slot
162
173
  - [`agent.ts`](./agent-ts.md) for runtime config
163
174
  - [Skills](./skills.md) for on-demand procedures
164
- - [Tools](./tools.md) for typed integrations
175
+ - [Tools](./tools.mdx) for typed integrations
165
176
  - [Sessions And Streaming](./runs-and-streaming.md) for the durable session model
@@ -8,6 +8,17 @@ Ash supports human-in-the-loop (HITL) in two forms:
8
8
  - tool approval requests
9
9
  - explicit user questions via the framework `ask_question` tool
10
10
 
11
+ <CopyPrompt text="Add human-in-the-loop behavior to the user's Ash project. Ash supports tool approval requests and explicit user questions through a unified InputRequest/InputResponse flow: the model requests input, Ash emits input.requested, the session parks at session.waiting, and the client or channel resumes with inputResponses or a matching follow-up message. Inspect agent/tools, agent/connections, and client or channel code, add needsApproval or connection approval with always, once, or never for sensitive actions, rely on ask_question for model-requested clarification, surface pending requests in the UI or channel, verify the parked-session resume flow, and do not commit unless the user asks.">
12
+ Add human-in-the-loop behavior to the user's Ash project. Ash supports tool approval requests and
13
+ explicit user questions through a unified InputRequest/InputResponse flow: the model requests
14
+ input, Ash emits input.requested, the session parks at session.waiting, and the client or channel
15
+ resumes with inputResponses or a matching follow-up message. Inspect agent/tools,
16
+ agent/connections, and client or channel code, add needsApproval or connection approval with
17
+ always, once, or never for sensitive actions, rely on ask_question for model-requested
18
+ clarification, surface pending requests in the UI or channel, verify the parked-session resume
19
+ flow, and do not commit unless the user asks.
20
+ </CopyPrompt>
21
+
11
22
  Both use the same unified `InputRequest` / `InputResponse` protocol:
12
23
 
13
24
  1. the model requests input
@@ -250,5 +261,5 @@ If you press `Escape` and then send a normal message, the pending requests are i
250
261
  ## What To Read Next
251
262
 
252
263
  - [`agent.ts`](./agent-ts.md)
253
- - [Tools](./tools.md)
264
+ - [Tools](./tools.mdx)
254
265
  - [Sessions And Streaming](./runs-and-streaming.md)
@@ -8,6 +8,7 @@
8
8
  "human-in-the-loop",
9
9
  "sandbox",
10
10
  "channels",
11
+ "frontend",
11
12
  "subagents",
12
13
  "schedules",
13
14
  "connections",
@@ -441,7 +441,7 @@ Important behavior:
441
441
 
442
442
  ## What To Read Next
443
443
 
444
- - [Tools](./tools.md)
444
+ - [Tools](./tools.mdx)
445
445
  - [Workspace](./workspace.md)
446
446
  - [Session Context](./session-context.md)
447
447
  - [Vercel Sandbox](https://vercel.com/docs/sandbox) — platform documentation for Vercel Sandbox
@@ -7,6 +7,15 @@ Schedules let an agent initiate recurring work — digests, syncs, maintenance,
7
7
 
8
8
  Schedules live under `schedules/` at the root agent package.
9
9
 
10
+ <CopyPrompt text="Add a recurring schedule to the user's Ash project. Ash schedules are root-only files under agent/schedules and provide exactly one of markdown or run. Inspect existing schedules and channel files, choose a .md fire-and-forget prompt or a TypeScript defineSchedule handler with cron and run, use receive with message, args, and appAuth plus waitUntil for channel handoff when needed, keep the schedule name path-derived, verify discovery and hosted Vercel Cron output when relevant, and do not commit unless the user asks.">
11
+ Add a recurring schedule to the user's Ash project. Ash schedules are root-only files under
12
+ agent/schedules and provide exactly one of markdown or run. Inspect existing schedules and channel
13
+ files, choose a .md fire-and-forget prompt or a TypeScript defineSchedule handler with cron and
14
+ run, use receive with message, args, and appAuth plus waitUntil for channel handoff when needed,
15
+ keep the schedule name path-derived, verify discovery and hosted Vercel Cron output when relevant,
16
+ and do not commit unless the user asks.
17
+ </CopyPrompt>
18
+
10
19
  ## Boundaries
11
20
 
12
21
  - schedules are root-only — local subagents cannot declare `schedules/`
@@ -205,5 +205,5 @@ For most apps:
205
205
  ## What To Read Next
206
206
 
207
207
  - [Context Control](./context-control.md)
208
- - [Tools](./tools.md)
209
- - [Subagents](./subagents.md)
208
+ - [Tools](./tools.mdx)
209
+ - [Subagents](./subagents.mdx)
@@ -11,6 +11,16 @@ A subagent is just an agent that lives under `agent/subagents/<id>/`. It is auth
11
11
  `defineAgent` helper as the root agent — the location under `subagents/` is what makes it a
12
12
  subagent.
13
13
 
14
+ <CopyPrompt text="Add a specialist subagent to the user's Ash project. Local subagents live under agent/subagents/id, require agent.ts plus instructions.md or instructions.ts, and need a description so the parent agent knows when to delegate. Inspect the existing subagent tree, choose a local subagent or defineRemoteAgent as appropriate, add focused instructions and any scoped tools, skills, sandbox files, or nested subagents, remember that skills and sandboxes do not cross the parent-child boundary, verify the parent sees the lowered subagent tool and child session stream, and do not commit unless the user asks.">
15
+ Add a specialist subagent to the user's Ash project. Local subagents live under
16
+ agent/subagents/id, require agent.ts plus instructions.md or instructions.ts, and need a
17
+ description so the parent agent knows when to delegate. Inspect the existing subagent tree, choose
18
+ a local subagent or defineRemoteAgent as appropriate, add focused instructions and any scoped
19
+ tools, skills, sandbox files, or nested subagents, remember that skills and sandboxes do not cross
20
+ the parent-child boundary, verify the parent sees the lowered subagent tool and child session
21
+ stream, and do not commit unless the user asks.
22
+ </CopyPrompt>
23
+
14
24
  ## What A Local Subagent Looks Like
15
25
 
16
26
  ```text
@@ -8,6 +8,17 @@ Tools are Ash's typed executable integrations.
8
8
  Use a tool when the model should call real code with structured input and get structured output
9
9
  back.
10
10
 
11
+ <CopyPrompt text="Add a typed Ash tool to the user's project. Ash tools live under agent/tools, and the model-facing tool name comes from the filename. Inspect existing agent/tools and agent/lib, then create or update the smallest agent/tools/snake_case_name.ts file using defineTool from experimental-ash/tools with a Zod or Standard Schema inputSchema and an inline execute(input, ctx). Use ctx.session, ctx.getSandbox(), or ctx.getSkill only inside runtime execution when needed, keep secrets in environment variables, bound large outputs or use toModelOutput and retentionPolicy, verify with the project's typecheck and tests, and do not commit unless the user asks.">
12
+ Add a typed Ash tool to the user's project. Ash tools live under agent/tools, and the model-facing
13
+ tool name comes from the filename. Inspect existing agent/tools and agent/lib, then create or
14
+ update the smallest agent/tools/snake_case_name.ts file using defineTool from
15
+ experimental-ash/tools with a Zod or Standard Schema inputSchema and an inline execute(input,
16
+ ctx). Use ctx.session, ctx.getSandbox(), or ctx.getSkill only inside runtime execution when
17
+ needed, keep secrets in environment variables, bound large outputs or use toModelOutput and
18
+ retentionPolicy, verify with the project's typecheck and tests, and do not commit unless the user
19
+ asks.
20
+ </CopyPrompt>
21
+
11
22
  ## The Main API
12
23
 
13
24
  `agent/tools/get_weather.ts`
@@ -362,7 +373,7 @@ platform-specific output (e.g. Slack Block Kit) from structured tool data that t
362
373
  needs to see.
363
374
 
364
375
  Use `toolResultFrom(result, toolDefinition)` to narrow an `action.result` to a specific tool and
365
- get typed output. See [Hooks — Narrowing tool results](./hooks.md#narrowing-tool-results) for the
376
+ get typed output. See [Hooks — Narrowing tool results](./hooks.mdx#narrowing-tool-results) for the
366
377
  full API.
367
378
 
368
379
  Note: `retentionPolicy` and `onCompact` operate on the projected output (what the model sees),
@@ -370,52 +381,52 @@ not the full `execute` return.
370
381
 
371
382
  ## Dynamic Tools
372
383
 
373
- Dynamic tools resolve at runtime based on session context — tenant configuration, feature flags, user roles, or external data. Use `defineTools` with an `events` object to produce tools dynamically.
384
+ Dynamic tools resolve at runtime based on session context — tenant configuration, feature flags, user roles, or external data. Use `defineDynamic` with an `events` object to produce tools dynamically.
374
385
 
375
386
  ### Single Dynamic Tool
376
387
 
377
388
  `agent/tools/analytics.ts`
378
389
 
379
390
  ```ts
380
- import { defineTool } from "experimental-ash/tools";
391
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
381
392
  import { z } from "zod";
382
393
 
383
- export default defineTool({
394
+ export default defineDynamic({
384
395
  events: {
385
396
  "session.started": async (event, ctx) => {
386
397
  const flags = await fetchFeatureFlags(ctx.session.id);
387
398
  if (!flags.advancedAnalytics) return null;
388
399
 
389
- return {
400
+ return defineTool({
390
401
  description: "Run an advanced analytics query.",
391
402
  inputSchema: z.object({ query: z.string() }),
392
403
  async execute(input) {
393
404
  return runAnalytics(input.query);
394
405
  },
395
- };
406
+ });
396
407
  },
397
408
  },
398
409
  });
399
410
  ```
400
411
 
401
- Return `null` to produce no tool. The tool name is the file slug (`analytics`), identical to a static `defineTool`.
412
+ Return `null` to produce no tool. The tool name is the file slug (`analytics`), identical to a static `defineTool`. Entries must be wrapped with `defineTool()` — this stamps the entry for the bundler transform so `execute` functions survive workflow step boundaries.
402
413
 
403
414
  ### Multiple Dynamic Tools
404
415
 
405
416
  `agent/tools/tenant.ts`
406
417
 
407
418
  ```ts
408
- import { defineTools, tool } from "experimental-ash/tools";
419
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
409
420
  import { z } from "zod";
410
421
 
411
- export default defineTools({
422
+ export default defineDynamic({
412
423
  events: {
413
424
  "session.started": async (event, ctx) => {
414
425
  const tenant = await fetchTenant(ctx.session.id);
415
426
  if (!tenant) return null;
416
427
 
417
428
  return {
418
- export: tool({
429
+ export: defineTool({
419
430
  description: `Export ${tenant.name} data`,
420
431
  inputSchema: z.object({ format: z.enum(["csv", "json"]) }),
421
432
  async execute(input) {
@@ -423,7 +434,7 @@ export default defineTools({
423
434
  return callTenantApi(tenant.apiUrl, "export", input.format);
424
435
  },
425
436
  }),
426
- query: tool({
437
+ query: defineTool({
427
438
  description: `Query ${tenant.name} data`,
428
439
  inputSchema: z.object({ sql: z.string() }),
429
440
  async execute(input) {
@@ -437,47 +448,62 @@ export default defineTools({
437
448
  });
438
449
  ```
439
450
 
440
- Each entry must be wrapped with `tool()` — it captures the schema type so `execute(input)` is inferred from `inputSchema`, matching the AI SDK's `tool()` pattern.
451
+ Each entry must be wrapped with `defineTool()` — it captures the schema type so `execute(input)` is inferred from `inputSchema`, matching the AI SDK's `tool()` pattern.
441
452
 
442
453
  ### Tool Naming
443
454
 
444
455
  Dynamic tool names are derived from the file path and the definition function:
445
456
 
446
- | Definition | File | Entry keys | Tool name(s) |
447
- | ------------- | -------------------------- | ----------------- | --------------------------------- |
448
- | `defineTool` | `agent/tools/analytics.ts` | _(single entry)_ | `analytics` |
449
- | `defineTools` | `agent/tools/tenant.ts` | `export`, `query` | `tenant__export`, `tenant__query` |
450
- | `defineTools` | `agent/tools/search.ts` | `run` | `search__run` |
457
+ | Definition | File | Entry keys | Tool name(s) |
458
+ | ------------------------------- | -------------------------- | ----------------- | --------------------------------- |
459
+ | `defineDynamic` (single return) | `agent/tools/analytics.ts` | _(single entry)_ | `analytics` |
460
+ | `defineDynamic` (map return) | `agent/tools/tenant.ts` | `export`, `query` | `tenant__export`, `tenant__query` |
461
+ | `defineDynamic` (map return) | `agent/tools/search.ts` | `run` | `search__run` |
462
+
463
+ `defineDynamic` with a single return always produces one tool named after the file slug — identical to static tools. `defineDynamic` with a map return always uses `slug__key`, even when the resolver returns a single entry. This keeps names stable: adding a second entry to a `defineDynamic` file does not rename the first.
464
+
465
+ ### Events
451
466
 
452
- `defineTool` always produces one tool named after the file slug — identical to static tools. `defineTools` always uses `slug__key`, even when the resolver returns a single entry. This keeps names stable: adding a second entry to a `defineTools` file does not rename the first.
467
+ The `events` object supports three event types:
453
468
 
454
- ### Event Scopes
469
+ | Event | When the resolver runs | Tools available for |
470
+ | ----------------- | ---------------------- | ------------------------------- |
471
+ | `session.started` | Once per session | Every model call in the session |
472
+ | `turn.started` | Once per turn | Every model call in the turn |
473
+ | `step.started` | Before each model call | That model call |
455
474
 
456
- The `events` object uses the same event names as hook stream events:
475
+ The bundler transform ensures resolvers survive workflow step boundaries — subsequent steps reconstruct execute functions from stored closure variables without re-running the resolver. Resolvers run concurrently — if multiple dynamic tool files declare the same event, their I/O overlaps.
457
476
 
458
- | Event | When the resolver runs | I/O behavior |
459
- | ----------------- | ----------------------- | ----------------------------------- |
460
- | `session.started` | Once per session | Runs once; results cached durably |
461
- | `turn.started` | Once per turn | Runs once per turn; cached for turn |
462
- | `step.started` | Before every model call | Runs every step; no caching |
477
+ ### Execution Order
463
478
 
464
- Session-scoped resolvers can do I/O (fetch from a database, call an API). The framework's bundler transform ensures the I/O executes once per session — subsequent workflow steps reconstruct execute functions from stored closure variables without re-running the resolver. Resolvers run concurrently — if multiple dynamic tool files declare the same event, their I/O overlaps.
479
+ When a stream event fires, three things happen in order:
480
+
481
+ 1. **Emit** — the channel adapter handler runs (can transform the event), then the event is written to the durable stream.
482
+ 2. **Hooks** — stream event hooks fire (typed handlers first, then `*` wildcard).
483
+ 3. **Dynamic tool resolvers** — resolvers subscribed to the event type run and update the tool set.
484
+
485
+ The tool-loop reads the current tool set right before each model call. If an event updated tools since the last read, the model sees the updated set.
465
486
 
466
487
  ### Multiple Events
467
488
 
468
489
  A single file can declare handlers for multiple events. The result of the most recently fired handler owns the tool set for that file:
469
490
 
470
491
  ```ts
471
- import { defineTools, tool } from "experimental-ash/tools";
492
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
472
493
 
473
- export default defineTools({
494
+ export default defineDynamic({
474
495
  events: {
475
496
  "session.started": async (event, ctx) => {
476
- return { query: tool({ description: "Query", inputSchema: {}, execute: async () => {} }) };
497
+ return {
498
+ query: defineTool({ description: "Query", inputSchema: {}, execute: async () => {} }),
499
+ };
477
500
  },
478
501
  "turn.started": async (event, ctx) => {
479
- // Turn handler replaces session handler's tools for this file
480
- return { search: tool({ description: "Search", inputSchema: {}, execute: async () => {} }) };
502
+ // On each turn, re-resolve tools. Replaces this file's
503
+ // session.started tools for subsequent calls.
504
+ return {
505
+ search: defineTool({ description: "Search", inputSchema: {}, execute: async () => {} }),
506
+ };
481
507
  },
482
508
  },
483
509
  });
@@ -485,11 +511,11 @@ export default defineTools({
485
511
 
486
512
  ### Type Inference
487
513
 
488
- `inputSchema` accepts Zod, Standard Schema, or plain JSON Schema — same as static `defineTool`. When using Zod, the `tool()` wrapper infers the `execute` input type automatically:
514
+ `inputSchema` accepts Zod, Standard Schema, or plain JSON Schema — same as static `defineTool`. When using Zod, the `defineTool()` wrapper infers the `execute` input type automatically:
489
515
 
490
516
  ```ts
491
517
  return {
492
- query: tool({
518
+ query: defineTool({
493
519
  inputSchema: z.object({ city: z.string() }),
494
520
  async execute(input) {
495
521
  // input.city is typed as string
@@ -506,17 +532,17 @@ The `execute` signature matches static tools: `execute(input: TInput, ctx: ToolC
506
532
  `execute` can live inside helper functions, `.map()` callbacks, or IIFEs — the bundler transform follows nested scopes and captures all referenced variables:
507
533
 
508
534
  ```ts
509
- import { defineTools, tool } from "experimental-ash/tools";
535
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
510
536
  import { z } from "zod";
511
537
 
512
- export default defineTools({
538
+ export default defineDynamic({
513
539
  events: {
514
540
  "session.started": async (event, ctx) => {
515
541
  const tenant = await fetchTenant(ctx.session.id);
516
542
 
517
543
  function buildTool(action: string) {
518
544
  const endpoint = `${tenant.apiUrl}/${action}`;
519
- return tool({
545
+ return defineTool({
520
546
  description: `${action} for ${tenant.name}`,
521
547
  inputSchema: z.object({ query: z.string() }),
522
548
  async execute(input) {