experimental-ash 0.42.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 (155) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/docs/public/README.md +8 -8
  3. package/dist/docs/public/advanced/{auth-and-route-protection.md → auth-and-route-protection.mdx} +11 -0
  4. package/dist/docs/public/advanced/context-control.md +4 -4
  5. package/dist/docs/public/advanced/{evals.md → evals.mdx} +11 -1
  6. package/dist/docs/public/advanced/{hooks.md → hooks.mdx} +28 -40
  7. package/dist/docs/public/advanced/instrumentation.md +92 -3
  8. package/dist/docs/public/advanced/project-layout.md +5 -5
  9. package/dist/docs/public/advanced/runs-and-streaming.md +8 -2
  10. package/dist/docs/public/advanced/session-context.md +1 -1
  11. package/dist/docs/public/advanced/typescript-api.md +49 -6
  12. package/dist/docs/public/advanced/vercel-deployment.md +1 -1
  13. package/dist/docs/public/agent-ts.md +5 -5
  14. package/dist/docs/public/channels/{discord.md → discord.mdx} +11 -0
  15. package/dist/docs/public/channels/index.md +10 -10
  16. package/dist/docs/public/channels/{slack.md → slack.mdx} +11 -0
  17. package/dist/docs/public/channels/{teams.md → teams.mdx} +12 -0
  18. package/dist/docs/public/channels/{telegram.md → telegram.mdx} +11 -0
  19. package/dist/docs/public/channels/{twilio.md → twilio.mdx} +11 -0
  20. package/dist/docs/public/{connections.md → connections.mdx} +18 -6
  21. package/dist/docs/public/frontend/README.md +16 -0
  22. package/dist/docs/public/frontend/meta.json +3 -0
  23. package/dist/docs/public/frontend/nextjs.md +192 -0
  24. package/dist/docs/public/frontend/use-ash-agent.md +332 -0
  25. package/dist/docs/public/{getting-started.md → getting-started.mdx} +12 -1
  26. package/dist/docs/public/{human-in-the-loop.md → human-in-the-loop.mdx} +12 -1
  27. package/dist/docs/public/meta.json +1 -0
  28. package/dist/docs/public/sandbox.md +1 -1
  29. package/dist/docs/public/{schedules.md → schedules.mdx} +9 -0
  30. package/dist/docs/public/skills.md +2 -2
  31. package/dist/docs/public/{subagents.md → subagents.mdx} +10 -0
  32. package/dist/docs/public/{tools.md → tools.mdx} +41 -26
  33. package/dist/src/channel/adapter.d.ts +13 -0
  34. package/dist/src/channel/instrumentation.d.ts +10 -0
  35. package/dist/src/channel/instrumentation.js +1 -0
  36. package/dist/src/channel/send.js +1 -1
  37. package/dist/src/channel/types.d.ts +16 -0
  38. package/dist/src/cli/commands/channels.d.ts +2 -1
  39. package/dist/src/cli/commands/channels.js +1 -1
  40. package/dist/src/compiler/manifest.d.ts +13 -1
  41. package/dist/src/compiler/manifest.js +1 -1
  42. package/dist/src/compiler/module-map.js +1 -1
  43. package/dist/src/compiler/normalize-manifest.js +1 -1
  44. package/dist/src/compiler/normalize-skill.d.ts +15 -2
  45. package/dist/src/compiler/normalize-skill.js +1 -1
  46. package/dist/src/compiler/normalize-tool.js +1 -1
  47. package/dist/src/context/dynamic-skill-lifecycle.d.ts +23 -0
  48. package/dist/src/context/dynamic-skill-lifecycle.js +1 -0
  49. package/dist/src/context/dynamic-tool-lifecycle.d.ts +2 -0
  50. package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
  51. package/dist/src/context/hook-lifecycle.d.ts +4 -6
  52. package/dist/src/context/hook-lifecycle.js +1 -1
  53. package/dist/src/context/keys.d.ts +6 -4
  54. package/dist/src/context/keys.js +1 -1
  55. package/dist/src/context/providers/connection.d.ts +9 -0
  56. package/dist/src/context/providers/connection.js +1 -1
  57. package/dist/src/context/providers/sandbox.js +1 -1
  58. package/dist/src/execution/ash-workflow-attributes.d.ts +118 -0
  59. package/dist/src/execution/ash-workflow-attributes.js +1 -0
  60. package/dist/src/execution/channel-context.d.ts +5 -0
  61. package/dist/src/execution/channel-context.js +1 -0
  62. package/dist/src/execution/create-session-step.d.ts +28 -1
  63. package/dist/src/execution/create-session-step.js +1 -1
  64. package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
  65. package/dist/src/execution/durable-session-store.d.ts +7 -0
  66. package/dist/src/execution/runtime-context.js +1 -1
  67. package/dist/src/execution/sandbox/prewarm.js +1 -1
  68. package/dist/src/execution/session.d.ts +6 -0
  69. package/dist/src/execution/session.js +2 -2
  70. package/dist/src/execution/skills/instructions.d.ts +3 -2
  71. package/dist/src/execution/subagent-tool.js +1 -1
  72. package/dist/src/execution/workflow-entry.js +1 -1
  73. package/dist/src/execution/workflow-steps.js +1 -1
  74. package/dist/src/harness/attachment-staging.js +1 -1
  75. package/dist/src/harness/code-mode.d.ts +0 -5
  76. package/dist/src/harness/code-mode.js +1 -1
  77. package/dist/src/harness/emission.d.ts +1 -1
  78. package/dist/src/harness/emission.js +1 -1
  79. package/dist/src/harness/instrumentation-config.d.ts +1 -1
  80. package/dist/src/harness/instrumentation-metadata.d.ts +23 -0
  81. package/dist/src/harness/instrumentation-metadata.js +1 -0
  82. package/dist/src/harness/otel-integration.d.ts +2 -2
  83. package/dist/src/harness/otel-integration.js +1 -1
  84. package/dist/src/harness/step-hooks.js +1 -1
  85. package/dist/src/harness/tool-loop.js +1 -1
  86. package/dist/src/harness/turn-tag-state.d.ts +50 -0
  87. package/dist/src/harness/turn-tag-state.js +1 -0
  88. package/dist/src/harness/types.d.ts +11 -2
  89. package/dist/src/internal/application/package.js +1 -1
  90. package/dist/src/internal/authored-definition/schema-backed.d.ts +0 -1
  91. package/dist/src/internal/authored-definition/schema-backed.js +1 -1
  92. package/dist/src/internal/instrumentation.d.ts +39 -0
  93. package/dist/src/internal/instrumentation.js +1 -0
  94. package/dist/src/internal/workflow/builtins.d.ts +32 -0
  95. package/dist/src/internal/workflow/builtins.js +1 -1
  96. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.d.ts +1 -1
  97. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.js +1 -1
  98. package/dist/src/internal/workflow-bundle/workflow-core-shim.d.ts +34 -0
  99. package/dist/src/internal/workflow-bundle/workflow-core-shim.js +1 -1
  100. package/dist/src/internal/workflow-bundle/workflow-transformer.js +1 -1
  101. package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
  102. package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -2
  103. package/dist/src/public/channels/slack/attachments.js +1 -1
  104. package/dist/src/public/channels/slack/index.d.ts +1 -1
  105. package/dist/src/public/channels/slack/slackChannel.d.ts +6 -0
  106. package/dist/src/public/channels/slack/slackChannel.js +1 -1
  107. package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
  108. package/dist/src/public/channels/twilio/index.d.ts +1 -1
  109. package/dist/src/public/channels/twilio/twilioChannel.d.ts +6 -0
  110. package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
  111. package/dist/src/public/definitions/defineChannel.d.ts +12 -2
  112. package/dist/src/public/definitions/defineChannel.js +1 -1
  113. package/dist/src/public/definitions/hook.d.ts +3 -11
  114. package/dist/src/public/definitions/instrumentation.d.ts +88 -2
  115. package/dist/src/public/definitions/skill.d.ts +5 -0
  116. package/dist/src/public/definitions/tool.d.ts +25 -66
  117. package/dist/src/public/definitions/tool.js +1 -1
  118. package/dist/src/public/instrumentation/index.d.ts +1 -4
  119. package/dist/src/public/instrumentation/index.js +1 -1
  120. package/dist/src/public/skills/index.d.ts +2 -0
  121. package/dist/src/public/skills/index.js +1 -1
  122. package/dist/src/public/tools/index.d.ts +2 -2
  123. package/dist/src/public/tools/index.js +1 -1
  124. package/dist/src/runtime/agent/mock-model-adapter.js +4 -7
  125. package/dist/src/runtime/agent/mock-model-skill-selection.d.ts +9 -0
  126. package/dist/src/runtime/agent/mock-model-skill-selection.js +4 -0
  127. package/dist/src/runtime/attributes/emit.d.ts +73 -0
  128. package/dist/src/runtime/attributes/emit.js +1 -0
  129. package/dist/src/runtime/channels/registry.js +1 -1
  130. package/dist/src/runtime/connections/mcp-client.js +1 -1
  131. package/dist/src/runtime/framework-tools/code-mode-connection-auth.d.ts +2 -0
  132. package/dist/src/runtime/framework-tools/connection-search-dynamic.d.ts +34 -0
  133. package/dist/src/runtime/framework-tools/connection-search-dynamic.js +1 -0
  134. package/dist/src/runtime/framework-tools/index.d.ts +7 -5
  135. package/dist/src/runtime/framework-tools/index.js +1 -1
  136. package/dist/src/runtime/prompt/connections.js +1 -1
  137. package/dist/src/runtime/resolve-agent-graph.js +1 -1
  138. package/dist/src/runtime/resolve-agent.js +1 -1
  139. package/dist/src/runtime/resolve-dynamic-skill.d.ts +8 -0
  140. package/dist/src/runtime/resolve-dynamic-skill.js +1 -0
  141. package/dist/src/runtime/resolve-dynamic-tool.js +1 -1
  142. package/dist/src/runtime/sessions/compiled-agent-cache.js +1 -1
  143. package/dist/src/runtime/sessions/runtime-context-keys.js +1 -1
  144. package/dist/src/runtime/types.d.ts +13 -4
  145. package/dist/src/shared/dynamic-tool-definition.d.ts +51 -76
  146. package/dist/src/shared/dynamic-tool-definition.js +1 -1
  147. package/dist/src/shared/guards.d.ts +14 -0
  148. package/dist/src/shared/guards.js +1 -1
  149. package/dist/src/shared/skill-definition.d.ts +5 -4
  150. package/dist/src/shared/tool-definition.d.ts +12 -0
  151. package/package.json +2 -1
  152. package/dist/src/runtime/framework-tools/connection-search.d.ts +0 -57
  153. package/dist/src/runtime/framework-tools/connection-search.js +0 -1
  154. package/dist/src/runtime/framework-tools/connection-tools.d.ts +0 -55
  155. 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,23 +381,23 @@ 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, tool } 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 tool({
400
+ return defineTool({
390
401
  description: "Run an advanced analytics query.",
391
402
  inputSchema: z.object({ query: z.string() }),
392
403
  async execute(input) {
@@ -398,24 +409,24 @@ export default defineTool({
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`. Entries must be wrapped with `tool()` — this stamps the entry for the bundler transform so `execute` functions survive workflow step boundaries.
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,19 +448,19 @@ 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` |
451
462
 
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.
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.
453
464
 
454
465
  ### Events
455
466
 
@@ -478,17 +489,21 @@ The tool-loop reads the current tool set right before each model call. If an eve
478
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:
479
490
 
480
491
  ```ts
481
- import { defineTools, tool } from "experimental-ash/tools";
492
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
482
493
 
483
- export default defineTools({
494
+ export default defineDynamic({
484
495
  events: {
485
496
  "session.started": async (event, ctx) => {
486
- return { query: tool({ description: "Query", inputSchema: {}, execute: async () => {} }) };
497
+ return {
498
+ query: defineTool({ description: "Query", inputSchema: {}, execute: async () => {} }),
499
+ };
487
500
  },
488
501
  "turn.started": async (event, ctx) => {
489
502
  // On each turn, re-resolve tools. Replaces this file's
490
503
  // session.started tools for subsequent calls.
491
- return { search: tool({ description: "Search", inputSchema: {}, execute: async () => {} }) };
504
+ return {
505
+ search: defineTool({ description: "Search", inputSchema: {}, execute: async () => {} }),
506
+ };
492
507
  },
493
508
  },
494
509
  });
@@ -496,11 +511,11 @@ export default defineTools({
496
511
 
497
512
  ### Type Inference
498
513
 
499
- `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:
500
515
 
501
516
  ```ts
502
517
  return {
503
- query: tool({
518
+ query: defineTool({
504
519
  inputSchema: z.object({ city: z.string() }),
505
520
  async execute(input) {
506
521
  // input.city is typed as string
@@ -517,17 +532,17 @@ The `execute` signature matches static tools: `execute(input: TInput, ctx: ToolC
517
532
  `execute` can live inside helper functions, `.map()` callbacks, or IIFEs — the bundler transform follows nested scopes and captures all referenced variables:
518
533
 
519
534
  ```ts
520
- import { defineTools, tool } from "experimental-ash/tools";
535
+ import { defineDynamic, defineTool } from "experimental-ash/tools";
521
536
  import { z } from "zod";
522
537
 
523
- export default defineTools({
538
+ export default defineDynamic({
524
539
  events: {
525
540
  "session.started": async (event, ctx) => {
526
541
  const tenant = await fetchTenant(ctx.session.id);
527
542
 
528
543
  function buildTool(action: string) {
529
544
  const endpoint = `${tenant.apiUrl}/${action}`;
530
- return tool({
545
+ return defineTool({
531
546
  description: `${action} for ${tenant.name}`,
532
547
  inputSchema: z.object({ query: z.string() }),
533
548
  async execute(input) {
@@ -75,6 +75,8 @@ export interface FetchFileResult {
75
75
  readonly mediaType?: string;
76
76
  readonly filename?: string;
77
77
  }
78
+ export type ChannelInstrumentationMetadata = Readonly<Record<string, unknown>>;
79
+ export type ChannelInstrumentationMetadataProjector = (state: Record<string, unknown> | undefined) => ChannelInstrumentationMetadata;
78
80
  /**
79
81
  * Plain-object channel adapter with durable state, an optional inbound
80
82
  * delivery hook, event handlers, and optional attachment resolution.
@@ -122,6 +124,17 @@ export type ChannelAdapter<TCtx extends ChannelAdapterContext<any> = ChannelAdap
122
124
  * construction time.
123
125
  */
124
126
  readonly fetchFile?: (url: string) => Promise<Buffer | FetchFileResult | null>;
127
+ /**
128
+ * Framework-owned observability projection for the active channel.
129
+ *
130
+ * Channel implementations decide which state fields are safe and
131
+ * meaningful to expose to instrumentation callbacks. The harness reads
132
+ * the resulting projection through a seed context key rather than
133
+ * inspecting adapter state directly.
134
+ */
135
+ readonly instrumentation?: {
136
+ readonly metadata?: ChannelInstrumentationMetadataProjector;
137
+ };
125
138
  } & ChannelEventHandlers<TCtx>;
126
139
  /**
127
140
  * Produces the default {@link StepInput} when no custom adapter `deliver`
@@ -0,0 +1,10 @@
1
+ import type { ChannelAdapter, ChannelInstrumentationMetadata } from "#channel/adapter.js";
2
+ export interface ChannelInstrumentationProjection {
3
+ readonly kind: string;
4
+ readonly metadata: ChannelInstrumentationMetadata;
5
+ }
6
+ export declare function buildChannelInstrumentationProjection(input: {
7
+ readonly adapter: ChannelAdapter;
8
+ readonly channelName?: string;
9
+ readonly existingKind?: string;
10
+ }): ChannelInstrumentationProjection;
@@ -0,0 +1 @@
1
+ import{createLogger}from"#internal/logging.js";import{getAdapterKind}from"#channel/adapter.js";import{isInstrumentationChannelKind,resolveInstrumentationProjection}from"#internal/instrumentation.js";const log=createLogger(`channel.instrumentation`);function buildChannelInstrumentationProjection(e){let{adapter:t,channelName:n,existingKind:r}=e;return{kind:resolveKind({adapter:t,channelName:n,existingKind:r}),metadata:resolveMetadata(t)}}function resolveKind(e){let{adapter:r,channelName:i,existingKind:a}=e;if(a!==void 0)return a;if(i!==void 0&&i.length>0)return`channel:${i}`;let o=getAdapterKind(r);return isInstrumentationChannelKind(o)?o:`channel:${o}`}function resolveMetadata(e){let n=e.instrumentation?.metadata;return n===void 0?{}:resolveInstrumentationProjection({invoke:()=>n(e.state),log,source:getAdapterKind(e)})??{}}export{buildChannelInstrumentationProjection};