experimental-ash 0.42.0 → 0.44.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 +37 -0
- package/bin/ash.js +1 -0
- package/dist/docs/internals/mechanical-invariants.md +16 -0
- 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} +28 -40
- package/dist/docs/public/advanced/instrumentation.md +142 -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 +50 -7
- 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 +39 -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} +41 -26
- package/dist/src/channel/adapter.d.ts +13 -0
- package/dist/src/channel/compiled-channel.d.ts +4 -1
- package/dist/src/channel/compiled-channel.js +1 -1
- package/dist/src/channel/instrumentation.d.ts +10 -0
- package/dist/src/channel/instrumentation.js +1 -0
- package/dist/src/channel/routes.d.ts +8 -10
- 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/compiled/.vendor-stamp.json +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +12 -19
- package/dist/src/compiled/@vercel/sandbox/network-policy.d.ts +161 -0
- package/dist/src/compiled/just-bash/index.d.ts +15 -2
- package/dist/src/compiled/just-bash/network/types.d.ts +155 -0
- package/dist/src/compiler/artifacts.d.ts +1 -0
- package/dist/src/compiler/artifacts.js +1 -1
- package/dist/src/compiler/channel-instrumentation-types.d.ts +8 -0
- package/dist/src/compiler/channel-instrumentation-types.js +2 -0
- 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 +2 -0
- 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 +6 -4
- 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/runtime-context.js +1 -1
- package/dist/src/execution/sandbox/bindings/local.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
- package/dist/src/execution/sandbox/prewarm.js +1 -1
- package/dist/src/execution/sandbox/session.d.ts +6 -1
- package/dist/src/execution/sandbox/session.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 +11 -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/packages/ash-scaffold/src/web-template.js +1 -0
- package/dist/src/public/channels/discord/discordChannel.d.ts +5 -2
- package/dist/src/public/channels/index.d.ts +1 -1
- 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 +12 -8
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/public/channels/teams/teamsChannel.d.ts +5 -2
- package/dist/src/public/channels/telegram/telegramChannel.d.ts +5 -2
- 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 +12 -3
- package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
- package/dist/src/public/definitions/defineChannel.d.ts +17 -4
- 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 +1 -66
- package/dist/src/public/definitions/instrumentation.js +1 -1
- 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 +175 -1
- package/dist/src/public/instrumentation/index.js +1 -1
- package/dist/src/public/sandbox/index.d.ts +1 -0
- 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-channel.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 +51 -76
- 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/sandbox-network-policy.d.ts +23 -0
- package/dist/src/shared/sandbox-network-policy.js +1 -0
- package/dist/src/shared/sandbox-session.d.ts +15 -0
- 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)
|
|
@@ -359,6 +359,44 @@ or to apply it only after the template has been built, set it in `onSession`'s `
|
|
|
359
359
|
common pattern: leave the factory open so `bootstrap` can `git clone` and `npm install`, then
|
|
360
360
|
lock down via `onSession`.
|
|
361
361
|
|
|
362
|
+
`onSession`'s `use()` (and the factory) accept the full policy shape, including per-domain
|
|
363
|
+
`transform`s for **credential brokering** — injecting headers at the firewall so a secret
|
|
364
|
+
authenticates egress without ever entering the sandbox process. When the brokered credential is
|
|
365
|
+
known at session start, configuring it up front in `onSession` is the right choice:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
onSession: async ({ use }) => {
|
|
369
|
+
await use({
|
|
370
|
+
networkPolicy: {
|
|
371
|
+
allow: {
|
|
372
|
+
"github.com": [{ transform: [{ headers: { authorization: "Basic …" } }] }],
|
|
373
|
+
"*": [],
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
When the policy must change **during a turn** — to broker a credential resolved mid-turn, or to
|
|
381
|
+
tighten egress after fetching data — call `setNetworkPolicy` on the live sandbox handle from a
|
|
382
|
+
tool, hook, or channel event. It accepts the same policy shape:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const sandbox = await ctx.getSandbox();
|
|
386
|
+
await sandbox.setNetworkPolicy({
|
|
387
|
+
allow: {
|
|
388
|
+
"github.com": [{ transform: [{ headers: { authorization: "Basic …" } }] }],
|
|
389
|
+
"*": [],
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The `"*": []` catch-all keeps general egress open while the `transform` applies only to
|
|
395
|
+
`github.com`. The policy takes effect from the time the call resolves, so await it before the
|
|
396
|
+
egress you want governed. The local backend rejects `setNetworkPolicy` — just-bash takes its
|
|
397
|
+
network policy only at sandbox creation and runs no binaries to govern — so run-time updates are
|
|
398
|
+
a Vercel-backend capability.
|
|
399
|
+
|
|
362
400
|
### Timeout
|
|
363
401
|
|
|
364
402
|
Sandbox VMs shut down after a configurable timeout. Ash defaults to **30 minutes**. Set a different
|
|
@@ -441,7 +479,7 @@ Important behavior:
|
|
|
441
479
|
|
|
442
480
|
## What To Read Next
|
|
443
481
|
|
|
444
|
-
- [Tools](./tools.
|
|
482
|
+
- [Tools](./tools.mdx)
|
|
445
483
|
- [Workspace](./workspace.md)
|
|
446
484
|
- [Session Context](./session-context.md)
|
|
447
485
|
- [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,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 `
|
|
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 {
|
|
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) {
|
|
@@ -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 `
|
|
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,19 +448,19 @@ 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` |
|
|
451
462
|
|
|
452
|
-
`
|
|
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 {
|
|
492
|
+
import { defineDynamic, defineTool } from "experimental-ash/tools";
|
|
482
493
|
|
|
483
|
-
export default
|
|
494
|
+
export default defineDynamic({
|
|
484
495
|
events: {
|
|
485
496
|
"session.started": async (event, ctx) => {
|
|
486
|
-
return {
|
|
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 {
|
|
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 `
|
|
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:
|
|
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 {
|
|
535
|
+
import { defineDynamic, defineTool } from "experimental-ash/tools";
|
|
521
536
|
import { z } from "zod";
|
|
522
537
|
|
|
523
|
-
export default
|
|
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
|
|
545
|
+
return defineTool({
|
|
531
546
|
description: `${action} for ${tenant.name}`,
|
|
532
547
|
inputSchema: z.object({ query: z.string() }),
|
|
533
548
|
async execute(input) {
|