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.
- package/CHANGELOG.md +36 -0
- package/dist/docs/internals/hooks.md +53 -11
- package/dist/docs/public/README.md +8 -8
- package/dist/docs/public/advanced/{auth-and-route-protection.md → auth-and-route-protection.mdx} +11 -0
- package/dist/docs/public/advanced/context-control.md +4 -4
- package/dist/docs/public/advanced/{evals.md → evals.mdx} +11 -1
- package/dist/docs/public/advanced/{hooks.md → hooks.mdx} +37 -46
- package/dist/docs/public/advanced/instrumentation.md +92 -3
- package/dist/docs/public/advanced/project-layout.md +5 -5
- package/dist/docs/public/advanced/runs-and-streaming.md +8 -2
- package/dist/docs/public/advanced/session-context.md +1 -1
- package/dist/docs/public/advanced/typescript-api.md +49 -6
- package/dist/docs/public/advanced/vercel-deployment.md +1 -1
- package/dist/docs/public/agent-ts.md +5 -5
- package/dist/docs/public/channels/{discord.md → discord.mdx} +11 -0
- package/dist/docs/public/channels/index.md +10 -10
- package/dist/docs/public/channels/{slack.md → slack.mdx} +11 -0
- package/dist/docs/public/channels/{teams.md → teams.mdx} +12 -0
- package/dist/docs/public/channels/{telegram.md → telegram.mdx} +11 -0
- package/dist/docs/public/channels/{twilio.md → twilio.mdx} +11 -0
- package/dist/docs/public/{connections.md → connections.mdx} +18 -6
- package/dist/docs/public/frontend/README.md +16 -0
- package/dist/docs/public/frontend/meta.json +3 -0
- package/dist/docs/public/frontend/nextjs.md +192 -0
- package/dist/docs/public/frontend/use-ash-agent.md +332 -0
- package/dist/docs/public/{getting-started.md → getting-started.mdx} +12 -1
- package/dist/docs/public/{human-in-the-loop.md → human-in-the-loop.mdx} +12 -1
- package/dist/docs/public/meta.json +1 -0
- package/dist/docs/public/sandbox.md +1 -1
- package/dist/docs/public/{schedules.md → schedules.mdx} +9 -0
- package/dist/docs/public/skills.md +2 -2
- package/dist/docs/public/{subagents.md → subagents.mdx} +10 -0
- package/dist/docs/public/{tools.md → tools.mdx} +62 -36
- package/dist/src/channel/adapter.d.ts +13 -0
- package/dist/src/channel/instrumentation.d.ts +10 -0
- package/dist/src/channel/instrumentation.js +1 -0
- package/dist/src/channel/send.js +1 -1
- package/dist/src/channel/types.d.ts +16 -0
- package/dist/src/cli/commands/channels.d.ts +2 -1
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/compiler/manifest.d.ts +13 -1
- package/dist/src/compiler/manifest.js +1 -1
- package/dist/src/compiler/module-map.js +1 -1
- package/dist/src/compiler/normalize-manifest.js +1 -1
- package/dist/src/compiler/normalize-skill.d.ts +15 -2
- package/dist/src/compiler/normalize-skill.js +1 -1
- package/dist/src/compiler/normalize-tool.js +1 -1
- package/dist/src/context/dynamic-skill-lifecycle.d.ts +23 -0
- package/dist/src/context/dynamic-skill-lifecycle.js +1 -0
- package/dist/src/context/dynamic-tool-lifecycle.d.ts +19 -23
- package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
- package/dist/src/context/hook-lifecycle.d.ts +4 -6
- package/dist/src/context/hook-lifecycle.js +1 -1
- package/dist/src/context/keys.d.ts +10 -9
- package/dist/src/context/keys.js +1 -1
- package/dist/src/context/providers/connection.d.ts +9 -0
- package/dist/src/context/providers/connection.js +1 -1
- package/dist/src/context/providers/sandbox.js +1 -1
- package/dist/src/execution/ash-workflow-attributes.d.ts +118 -0
- package/dist/src/execution/ash-workflow-attributes.js +1 -0
- package/dist/src/execution/channel-context.d.ts +5 -0
- package/dist/src/execution/channel-context.js +1 -0
- package/dist/src/execution/create-session-step.d.ts +28 -1
- package/dist/src/execution/create-session-step.js +1 -1
- package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
- package/dist/src/execution/durable-session-store.d.ts +7 -0
- package/dist/src/execution/node-step.d.ts +2 -2
- package/dist/src/execution/node-step.js +1 -1
- package/dist/src/execution/runtime-context.js +1 -1
- package/dist/src/execution/sandbox/prewarm.js +1 -1
- package/dist/src/execution/session.d.ts +6 -0
- package/dist/src/execution/session.js +2 -2
- package/dist/src/execution/skills/instructions.d.ts +3 -2
- package/dist/src/execution/subagent-tool.js +1 -1
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/execution/workflow-steps.js +1 -1
- package/dist/src/harness/attachment-staging.js +1 -1
- package/dist/src/harness/code-mode.d.ts +0 -5
- package/dist/src/harness/code-mode.js +1 -1
- package/dist/src/harness/emission.d.ts +1 -1
- package/dist/src/harness/emission.js +1 -1
- package/dist/src/harness/instrumentation-config.d.ts +1 -1
- package/dist/src/harness/instrumentation-metadata.d.ts +23 -0
- package/dist/src/harness/instrumentation-metadata.js +1 -0
- package/dist/src/harness/otel-integration.d.ts +2 -2
- package/dist/src/harness/otel-integration.js +1 -1
- package/dist/src/harness/step-hooks.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/harness/turn-tag-state.d.ts +50 -0
- package/dist/src/harness/turn-tag-state.js +1 -0
- package/dist/src/harness/types.d.ts +20 -2
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/schema-backed.d.ts +0 -1
- package/dist/src/internal/authored-definition/schema-backed.js +1 -1
- package/dist/src/internal/instrumentation.d.ts +39 -0
- package/dist/src/internal/instrumentation.js +1 -0
- package/dist/src/internal/workflow/builtins.d.ts +32 -0
- package/dist/src/internal/workflow/builtins.js +1 -1
- package/dist/src/internal/workflow-bundle/dynamic-tool-transform.d.ts +1 -1
- package/dist/src/internal/workflow-bundle/dynamic-tool-transform.js +1 -1
- package/dist/src/internal/workflow-bundle/workflow-core-shim.d.ts +34 -0
- package/dist/src/internal/workflow-bundle/workflow-core-shim.js +1 -1
- package/dist/src/internal/workflow-bundle/workflow-transformer.js +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
- package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -2
- package/dist/src/public/channels/slack/attachments.js +1 -1
- package/dist/src/public/channels/slack/index.d.ts +1 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +6 -0
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
- package/dist/src/public/channels/twilio/index.d.ts +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.d.ts +6 -0
- package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
- package/dist/src/public/definitions/defineChannel.d.ts +12 -2
- package/dist/src/public/definitions/defineChannel.js +1 -1
- package/dist/src/public/definitions/hook.d.ts +3 -11
- package/dist/src/public/definitions/instrumentation.d.ts +88 -2
- package/dist/src/public/definitions/skill.d.ts +5 -0
- package/dist/src/public/definitions/tool.d.ts +25 -66
- package/dist/src/public/definitions/tool.js +1 -1
- package/dist/src/public/instrumentation/index.d.ts +1 -4
- package/dist/src/public/instrumentation/index.js +1 -1
- package/dist/src/public/skills/index.d.ts +2 -0
- package/dist/src/public/skills/index.js +1 -1
- package/dist/src/public/tools/index.d.ts +2 -2
- package/dist/src/public/tools/index.js +1 -1
- package/dist/src/runtime/agent/mock-model-adapter.js +4 -7
- package/dist/src/runtime/agent/mock-model-skill-selection.d.ts +9 -0
- package/dist/src/runtime/agent/mock-model-skill-selection.js +4 -0
- package/dist/src/runtime/attributes/emit.d.ts +73 -0
- package/dist/src/runtime/attributes/emit.js +1 -0
- package/dist/src/runtime/channels/registry.js +1 -1
- package/dist/src/runtime/connections/mcp-client.js +1 -1
- package/dist/src/runtime/framework-tools/code-mode-connection-auth.d.ts +2 -0
- package/dist/src/runtime/framework-tools/connection-search-dynamic.d.ts +34 -0
- package/dist/src/runtime/framework-tools/connection-search-dynamic.js +1 -0
- package/dist/src/runtime/framework-tools/index.d.ts +7 -5
- package/dist/src/runtime/framework-tools/index.js +1 -1
- package/dist/src/runtime/prompt/connections.js +1 -1
- package/dist/src/runtime/resolve-agent-graph.js +1 -1
- package/dist/src/runtime/resolve-agent.js +1 -1
- package/dist/src/runtime/resolve-dynamic-skill.d.ts +8 -0
- package/dist/src/runtime/resolve-dynamic-skill.js +1 -0
- package/dist/src/runtime/resolve-dynamic-tool.js +1 -1
- package/dist/src/runtime/sessions/compiled-agent-cache.js +1 -1
- package/dist/src/runtime/sessions/runtime-context-keys.js +1 -1
- package/dist/src/runtime/types.d.ts +13 -4
- package/dist/src/shared/dynamic-tool-definition.d.ts +57 -80
- package/dist/src/shared/dynamic-tool-definition.js +1 -1
- package/dist/src/shared/guards.d.ts +14 -0
- package/dist/src/shared/guards.js +1 -1
- package/dist/src/shared/skill-definition.d.ts +5 -4
- package/dist/src/shared/tool-definition.d.ts +12 -0
- package/package.json +2 -1
- package/dist/src/runtime/framework-tools/connection-search.d.ts +0 -57
- package/dist/src/runtime/framework-tools/connection-search.js +0 -1
- package/dist/src/runtime/framework-tools/connection-tools.d.ts +0 -55
- 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.
|
|
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.
|
|
264
|
+
- [Tools](./tools.mdx)
|
|
254
265
|
- [Sessions And Streaming](./runs-and-streaming.md)
|
|
@@ -441,7 +441,7 @@ Important behavior:
|
|
|
441
441
|
|
|
442
442
|
## What To Read Next
|
|
443
443
|
|
|
444
|
-
- [Tools](./tools.
|
|
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/`
|
|
@@ -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.
|
|
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 `
|
|
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
|
|
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 {
|
|
419
|
+
import { defineDynamic, defineTool } from "experimental-ash/tools";
|
|
409
420
|
import { z } from "zod";
|
|
410
421
|
|
|
411
|
-
export default
|
|
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:
|
|
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:
|
|
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 `
|
|
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
|
|
447
|
-
|
|
|
448
|
-
| `
|
|
449
|
-
| `
|
|
450
|
-
| `
|
|
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
|
-
|
|
467
|
+
The `events` object supports three event types:
|
|
453
468
|
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
492
|
+
import { defineDynamic, defineTool } from "experimental-ash/tools";
|
|
472
493
|
|
|
473
|
-
export default
|
|
494
|
+
export default defineDynamic({
|
|
474
495
|
events: {
|
|
475
496
|
"session.started": async (event, ctx) => {
|
|
476
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
query: defineTool({ description: "Query", inputSchema: {}, execute: async () => {} }),
|
|
499
|
+
};
|
|
477
500
|
},
|
|
478
501
|
"turn.started": async (event, ctx) => {
|
|
479
|
-
//
|
|
480
|
-
|
|
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 `
|
|
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:
|
|
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 {
|
|
535
|
+
import { defineDynamic, defineTool } from "experimental-ash/tools";
|
|
510
536
|
import { z } from "zod";
|
|
511
537
|
|
|
512
|
-
export default
|
|
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
|
|
545
|
+
return defineTool({
|
|
520
546
|
description: `${action} for ${tenant.name}`,
|
|
521
547
|
inputSchema: z.object({ query: z.string() }),
|
|
522
548
|
async execute(input) {
|