experimental-ash 0.59.0 → 0.61.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 +12 -0
- package/dist/docs/public/advanced/auth-and-route-protection.mdx +8 -6
- package/dist/docs/public/agent-ts.md +17 -47
- package/dist/docs/public/channels/README.md +60 -0
- package/dist/docs/public/channels/ash.mdx +103 -0
- package/dist/docs/public/channels/custom.mdx +288 -0
- package/dist/docs/public/channels/discord.mdx +1 -3
- package/dist/docs/public/channels/github.md +1 -1
- package/dist/docs/public/channels/meta.json +3 -0
- package/dist/docs/public/channels/slack.mdx +29 -1
- package/dist/docs/public/channels/teams.mdx +1 -3
- package/dist/docs/public/channels/telegram.mdx +1 -3
- package/dist/docs/public/channels/twilio.mdx +1 -3
- package/dist/docs/public/frontend/nextjs.md +24 -8
- package/dist/docs/public/onboarding.md +4 -3
- package/dist/docs/public/schedules.mdx +4 -4
- package/dist/docs/public/tools.mdx +1 -1
- package/dist/src/channel/compiled-channel.d.ts +2 -6
- package/dist/src/channel/routes.d.ts +75 -7
- package/dist/src/channel/routes.js +1 -1
- package/dist/src/compiler/manifest.d.ts +4 -4
- package/dist/src/compiler/manifest.js +1 -1
- package/dist/src/execution/dispatch-code-mode-runtime-actions-step.d.ts +21 -0
- package/dist/src/execution/dispatch-code-mode-runtime-actions-step.js +1 -0
- package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
- package/dist/src/execution/next-driver-action.d.ts +5 -0
- package/dist/src/execution/node-step.js +1 -1
- package/dist/src/execution/turn-workflow.js +1 -1
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/execution/workflow-steps.d.ts +5 -0
- package/dist/src/execution/workflow-steps.js +1 -1
- package/dist/src/harness/code-mode-runtime-action-state.d.ts +6 -0
- package/dist/src/harness/code-mode-runtime-action-state.js +1 -0
- package/dist/src/harness/code-mode.js +1 -1
- package/dist/src/harness/runtime-actions.d.ts +1 -0
- package/dist/src/harness/runtime-actions.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/host/channel-routes.d.ts +2 -2
- package/dist/src/internal/nitro/host/channel-routes.js +2 -1
- package/dist/src/internal/nitro/host/create-application-nitro.js +1 -1
- package/dist/src/internal/nitro/routes/channel-dispatch.d.ts +2 -0
- package/dist/src/internal/nitro/routes/channel-dispatch.js +1 -1
- package/dist/src/internal/vercel-agent-summary.d.ts +2 -2
- package/dist/src/internal/vercel-agent-summary.js +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
- package/dist/src/public/channels/index.d.ts +1 -1
- package/dist/src/public/channels/index.js +1 -1
- package/dist/src/public/definitions/channel.d.ts +7 -0
- package/dist/src/public/definitions/defineChannel.d.ts +3 -6
- package/dist/src/public/definitions/defineChannel.js +1 -1
- package/dist/src/runtime/framework-channels/index.js +1 -1
- package/dist/src/runtime/resolve-channel.js +1 -1
- package/dist/src/runtime/types.d.ts +8 -3
- package/package.json +1 -1
- package/dist/docs/public/channels/attachments.md +0 -71
- package/dist/docs/public/channels/index.md +0 -661
|
@@ -1,661 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Channels"
|
|
3
|
-
description: "Deliver your agent over HTTP, Slack, GitHub, Discord, Twilio, Telegram, Microsoft Teams, and custom transports."
|
|
4
|
-
url: /channels
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Channels let Ash receive messages from external systems and deliver responses back to the same
|
|
8
|
-
conversation surface.
|
|
9
|
-
|
|
10
|
-
A channel is defined with `defineChannel()`. It declares routes, optional state, an optional
|
|
11
|
-
`context()` factory, and typed event handlers under `events`. Route handlers receive the inbound
|
|
12
|
-
request and a helpers object with `send()` and `getSession()`. The `send()` function is the unified
|
|
13
|
-
entry point for starting a new turn or resuming a session -- it replaces the old `ctx.agent.run()` and
|
|
14
|
-
`ctx.agent.deliver()` split.
|
|
15
|
-
|
|
16
|
-
The runtime and harness still own the model turn, tool execution, compaction, and session
|
|
17
|
-
persistence.
|
|
18
|
-
|
|
19
|
-
## Where Channels Fit
|
|
20
|
-
|
|
21
|
-
Ash ships the public HTTP protocol as channels:
|
|
22
|
-
|
|
23
|
-
- `ashChannel({ auth })` for the built-in Ash protocol channel
|
|
24
|
-
|
|
25
|
-
Ash also supports authored channels under `agent/channels/` for platform-specific integrations such
|
|
26
|
-
as Slack, GitHub, Discord, Twilio, Telegram, Microsoft Teams, or custom webhooks.
|
|
27
|
-
|
|
28
|
-
## Filesystem Shape
|
|
29
|
-
|
|
30
|
-
```text
|
|
31
|
-
agent/
|
|
32
|
-
agent.ts
|
|
33
|
-
instructions.md
|
|
34
|
-
channels/
|
|
35
|
-
slack.ts
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Rules that matter:
|
|
39
|
-
|
|
40
|
-
- `channels/` is root-only today.
|
|
41
|
-
- local subagents do not declare channels.
|
|
42
|
-
- channel files are module-backed only.
|
|
43
|
-
- the file stem becomes the channel id.
|
|
44
|
-
|
|
45
|
-
## The Main API
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
import { defineChannel, POST, GET } from "experimental-ash/channels";
|
|
49
|
-
|
|
50
|
-
export default defineChannel({
|
|
51
|
-
routes: [
|
|
52
|
-
POST("/message", async (req, { send, getSession, waitUntil }) => {
|
|
53
|
-
const body = await req.json();
|
|
54
|
-
const session = await send(body.message, {
|
|
55
|
-
auth: null,
|
|
56
|
-
continuationToken: body.token,
|
|
57
|
-
});
|
|
58
|
-
return Response.json({ sessionId: session.id });
|
|
59
|
-
}),
|
|
60
|
-
GET("/sessions/:sessionId/stream", async (req, { getSession, params }) => {
|
|
61
|
-
const session = getSession(params.sessionId);
|
|
62
|
-
const stream = await session.getEventStream();
|
|
63
|
-
return new Response(stream, {
|
|
64
|
-
headers: { "content-type": "text/event-stream" },
|
|
65
|
-
});
|
|
66
|
-
}),
|
|
67
|
-
],
|
|
68
|
-
events: {
|
|
69
|
-
"message.completed"(event, ctx) {
|
|
70
|
-
// handle completed messages
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`defineChannel({ routes, ... })` defines a channel. Routes are declared with `POST()` and `GET()`
|
|
77
|
-
helpers. Each route handler receives the raw `Request` and a helpers object:
|
|
78
|
-
|
|
79
|
-
- `send(message, { auth, continuationToken, state? })` -- start or resume a session. Returns a
|
|
80
|
-
`Session`.
|
|
81
|
-
- `getSession(sessionId)` -- look up an existing session. The returned `Session` exposes
|
|
82
|
-
`getEventStream({ startIndex? })` for streaming.
|
|
83
|
-
- `params` -- route parameters extracted from the path pattern.
|
|
84
|
-
- `waitUntil(promise)` -- extend the request lifetime for background work.
|
|
85
|
-
|
|
86
|
-
Event handlers like `"message.completed"` are declared under the `events` key on the config object.
|
|
87
|
-
They receive `(eventData, channel, ctx)` where `channel` carries platform handles and session
|
|
88
|
-
continuation operations, and `ctx` is the Ash `SessionContext`. Use
|
|
89
|
-
`toolResultFrom` from `experimental-ash/tools` to narrow tool results (see
|
|
90
|
-
[Hooks — Narrowing tool results](../hooks.mdx#narrowing-tool-results)).
|
|
91
|
-
|
|
92
|
-
## The Ash Channel
|
|
93
|
-
|
|
94
|
-
The framework ships its canonical session protocol on `experimental-ash/channels/ash`:
|
|
95
|
-
|
|
96
|
-
- `ashChannel({ auth, onMessage?, events? })`
|
|
97
|
-
|
|
98
|
-
This is the channel the Ash dev client and any deployed-agent SDK talk to — it mounts the routes
|
|
99
|
-
under `/ash/v1/session*` documented in [Ash External Agent Protocol](../../external-agent-protocol.md).
|
|
100
|
-
|
|
101
|
-
`auth` accepts either a single `AuthFn` or an ordered array walked in order: the first entry
|
|
102
|
-
returning a `SessionAuthContext` wins, `null` / `undefined` skips, exhaustion (including `[]`)
|
|
103
|
-
returns `401`. Use `experimental-ash/channels/auth` for the common helpers such as `localDev()`,
|
|
104
|
-
`vercelOidc()`, `httpBasic()`, and `none()`.
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { ashChannel } from "experimental-ash/channels/ash";
|
|
108
|
-
import { localDev, vercelOidc } from "experimental-ash/channels/auth";
|
|
109
|
-
|
|
110
|
-
export default ashChannel({
|
|
111
|
-
auth: [localDev(), vercelOidc()],
|
|
112
|
-
});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Pass `onMessage` to add authenticated context before the Ash HTTP channel dispatches a user
|
|
116
|
-
message. The hook runs after `auth` succeeds and body parsing completes, so `ctx.ash.caller` is
|
|
117
|
-
available for caller-specific lookups. Return `{ auth, context }` to choose the runtime session
|
|
118
|
-
auth and append context strings as user messages before the inbound message, or `null` to accept
|
|
119
|
-
the request without dispatching a turn. When `onMessage` is omitted, Ash dispatches with
|
|
120
|
-
`defaultAshAuth(ctx)`.
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
import { ashChannel, defaultAshAuth } from "experimental-ash/channels/ash";
|
|
124
|
-
|
|
125
|
-
export default ashChannel({
|
|
126
|
-
auth: [localDev(), vercelOidc()],
|
|
127
|
-
async onMessage(ctx, message) {
|
|
128
|
-
const auth = defaultAshAuth(ctx);
|
|
129
|
-
const profile = auth ? await loadCallerProfile(auth.principalId) : null;
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
auth,
|
|
133
|
-
context: profile ? [`Caller profile:\n${JSON.stringify(profile)}`] : [],
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Pass `events` to observe runtime stream events on the default HTTP channel. The Ash channel has no
|
|
140
|
-
platform-specific context, so `channel` exposes session continuation operations and `ctx` exposes
|
|
141
|
-
the current session context:
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
export default ashChannel({
|
|
145
|
-
auth: [localDev(), vercelOidc()],
|
|
146
|
-
events: {
|
|
147
|
-
"turn.started"(_event, _channel, ctx) {
|
|
148
|
-
auditTurn(ctx.session.id);
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
`pnpm create experimental-ash-agent` scaffolds the Web Chat example channel at
|
|
155
|
-
`agent/channels/ash.ts`. It permits Vercel OIDC and localhost requests and includes an
|
|
156
|
-
`exampleProductionAuth()` placeholder that throws in production until you replace it
|
|
157
|
-
with end-user auth. If you delete the authored file, Ash falls back to
|
|
158
|
-
`[localDev(), vercelOidc()]`; that default does not admit browser users in production.
|
|
159
|
-
See [Auth and Route Protection](../auth-and-route-protection.mdx) for the full walking
|
|
160
|
-
semantics and helper reference.
|
|
161
|
-
|
|
162
|
-
## Slack Channels
|
|
163
|
-
|
|
164
|
-
Slack channels are authored with a single `slackChannel()` factory. Pass no config for the
|
|
165
|
-
zero-config defaults, or supply only the fields you want to override -- everything else keeps the
|
|
166
|
-
default.
|
|
167
|
-
|
|
168
|
-
### Zero-config
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
import { slackChannel } from "experimental-ash/channels/slack";
|
|
172
|
-
|
|
173
|
-
export default slackChannel();
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Handles app mentions, direct messages, typing indicators, message delivery, and HITL interactions
|
|
177
|
-
out of the box. The default `onAppMention` and `onDirectMessage` derive auth from the inbound Slack
|
|
178
|
-
actor and post a `"Thinking…"` typing indicator before the workflow runtime starts.
|
|
179
|
-
|
|
180
|
-
### Custom config
|
|
181
|
-
|
|
182
|
-
When you need custom rendering or event handling, pass a config object. `onAppMention` decides
|
|
183
|
-
whether to dispatch a channel mention; `onDirectMessage` does the same for 1:1 DMs (`message`
|
|
184
|
-
events with `channel_type: "im"`); `events` lets you replace individual event handlers;
|
|
185
|
-
`onInteraction` handles non-HITL button clicks.
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { slackChannel } from "experimental-ash/channels/slack";
|
|
189
|
-
|
|
190
|
-
export default slackChannel({
|
|
191
|
-
onAppMention(ctx, message) {
|
|
192
|
-
if (!message.author) return null;
|
|
193
|
-
return {
|
|
194
|
-
auth: {
|
|
195
|
-
principalId: message.author.userId,
|
|
196
|
-
principalType: "user",
|
|
197
|
-
authenticator: "slack",
|
|
198
|
-
attributes: {},
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
},
|
|
202
|
-
onDirectMessage(ctx, message) {
|
|
203
|
-
if (!message.author) return null;
|
|
204
|
-
return {
|
|
205
|
-
auth: {
|
|
206
|
-
principalId: message.author.userId,
|
|
207
|
-
principalType: "user",
|
|
208
|
-
authenticator: "slack",
|
|
209
|
-
attributes: { surface: "im" },
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
events: {
|
|
214
|
-
"actions.requested"(event, ctx) {
|
|
215
|
-
const labels = event.actions.map((a) => (a.kind === "tool-call" ? a.toolName : a.kind));
|
|
216
|
-
ctx.thread.startTyping(`Running ${labels.join(", ")}...`);
|
|
217
|
-
},
|
|
218
|
-
"message.completed"(event, ctx) {
|
|
219
|
-
if (event.finishReason === "tool-calls") return;
|
|
220
|
-
if (event.message) ctx.thread.post(event.message);
|
|
221
|
-
},
|
|
222
|
-
"session.failed"(_event, ctx) {
|
|
223
|
-
ctx.thread.post("Something went wrong.");
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Fields you supply fully replace the corresponding default -- if you pass your own
|
|
230
|
-
`"message.completed"` handler, the default's behavior for that event is gone. Other defaults stay
|
|
231
|
-
in place. Direct messages require the Slack app to subscribe to `message.im` with the `im:history`
|
|
232
|
-
scope.
|
|
233
|
-
|
|
234
|
-
### Interactions
|
|
235
|
-
|
|
236
|
-
Interactions (button clicks, modals) are platform events, not agent events. They do not appear in the
|
|
237
|
-
event handlers.
|
|
238
|
-
|
|
239
|
-
`slackChannel()` handles interactions in two paths:
|
|
240
|
-
|
|
241
|
-
- **HITL interactions** (approval buttons matching pending input requests) are delivered to the agent
|
|
242
|
-
automatically. The agent resumes.
|
|
243
|
-
- **Custom interactions** (feedback buttons, SQL toggles) are passed to `onInteraction`:
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
import { slackChannel } from "experimental-ash/channels/slack";
|
|
247
|
-
|
|
248
|
-
export default slackChannel({
|
|
249
|
-
onAppMention(ctx, message) {
|
|
250
|
-
if (!message.author) return null;
|
|
251
|
-
return {
|
|
252
|
-
auth: {
|
|
253
|
-
principalId: message.author.userId,
|
|
254
|
-
principalType: "user",
|
|
255
|
-
authenticator: "slack",
|
|
256
|
-
attributes: {},
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
},
|
|
260
|
-
events: {
|
|
261
|
-
"message.completed"(event, ctx) {
|
|
262
|
-
if (event.finishReason === "tool-calls") return;
|
|
263
|
-
if (event.message) ctx.thread.post(event.message);
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
onInteraction(action, ctx) {
|
|
267
|
-
// handle custom button clicks
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### The two shapes
|
|
273
|
-
|
|
274
|
-
```text
|
|
275
|
-
defineChannel({ routes: [...], events: {...} }) -- raw: you handle HTTP
|
|
276
|
-
ashChannel({ auth, onMessage?, events? }) -- ash: default HTTP protocol
|
|
277
|
-
slackChannel({ onAppMention?, onDirectMessage?, events?, onInteraction? }) -- slack: everything wired, override as needed
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
`slackChannel(config)` compiles down to `defineChannel` plus typed inbound parsing and event
|
|
281
|
-
dispatch for app mentions, direct messages, and interactions.
|
|
282
|
-
|
|
283
|
-
For a Slack app backed by Vercel Connect, see [Slack channel setup](./slack.mdx) to create the Connect client
|
|
284
|
-
and channel file.
|
|
285
|
-
|
|
286
|
-
## GitHub Channels
|
|
287
|
-
|
|
288
|
-
GitHub channels are authored with `githubChannel()`:
|
|
289
|
-
|
|
290
|
-
```ts
|
|
291
|
-
import { githubChannel } from "experimental-ash/channels/github";
|
|
292
|
-
|
|
293
|
-
export default githubChannel({
|
|
294
|
-
botName: "my-agent",
|
|
295
|
-
});
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
The channel verifies GitHub App webhooks at `/ash/v1/github`, dispatches bot-directed issue/PR
|
|
299
|
-
comments and pull-request review comments, can opt into issue and pull-request hooks, exposes
|
|
300
|
-
GitHub-native issue, PR, reaction, and checkout helpers, and injects bounded PR metadata,
|
|
301
|
-
file lists, and patch text as turn `context`.
|
|
302
|
-
|
|
303
|
-
See [GitHub channel setup](./github.md) for GitHub App permissions, PR context, checkout, and
|
|
304
|
-
custom inbound hooks.
|
|
305
|
-
|
|
306
|
-
## Discord Channels
|
|
307
|
-
|
|
308
|
-
Discord channels are authored with `discordChannel()`:
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
import { discordChannel } from "experimental-ash/channels/discord";
|
|
312
|
-
|
|
313
|
-
export default discordChannel();
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
The channel verifies Discord's Ed25519 interaction signature headers, accepts HTTP Interactions at
|
|
317
|
-
`/ash/v1/discord`, responds to `PING`, dispatches application commands, and resolves HITL button,
|
|
318
|
-
select, and modal submissions back into Ash input responses. The default command parser uses a
|
|
319
|
-
string option named `message` as the agent prompt and derives auth from the invoking Discord user.
|
|
320
|
-
|
|
321
|
-
Default delivery edits the original deferred interaction response for the first agent reply, sends
|
|
322
|
-
followups after that, and falls back to bot-token channel messages when the interaction token can no
|
|
323
|
-
longer be used. Proactive `receive(discord, { channelId })` sessions also use bot-token channel
|
|
324
|
-
messages. Default progress handlers trigger Discord's short-lived typing indicator when bot auth is
|
|
325
|
-
available.
|
|
326
|
-
|
|
327
|
-
See [Discord channel setup](./discord.mdx) for endpoint setup, environment variables, command
|
|
328
|
-
registration, and overrides.
|
|
329
|
-
|
|
330
|
-
## Twilio Channels
|
|
331
|
-
|
|
332
|
-
Twilio channels are authored with `twilioChannel()`:
|
|
333
|
-
|
|
334
|
-
```ts
|
|
335
|
-
import { twilioChannel } from "experimental-ash/channels/twilio";
|
|
336
|
-
|
|
337
|
-
export default twilioChannel({
|
|
338
|
-
allowFrom: "+15551234567",
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
The channel verifies `X-Twilio-Signature` itself, accepts inbound SMS at
|
|
343
|
-
`/ash/v1/twilio/messages`, answers phone calls at `/ash/v1/twilio/voice`, and dispatches speech
|
|
344
|
-
transcripts posted to `/ash/v1/twilio/voice/transcription`. The raw continuation token is the
|
|
345
|
-
caller/sender phone number plus the Twilio receiver (`From:To`), so texts and call transcripts for
|
|
346
|
-
the same phone-number pair resume the same Ash session without collapsing conversations that use
|
|
347
|
-
different Twilio numbers. `allowFrom` accepts a single phone number, a list, or a zero-argument
|
|
348
|
-
resolver when the allowed phone numbers come from dynamic state. Use `allowFrom: "*"` only when the
|
|
349
|
-
`onText` and `onVoice` hooks perform their own incoming-number checks.
|
|
350
|
-
|
|
351
|
-
See [Twilio channel setup](./twilio.mdx) for webhook URLs, environment variables, and overrides.
|
|
352
|
-
|
|
353
|
-
## Telegram Channels
|
|
354
|
-
|
|
355
|
-
Telegram channels are authored with `telegramChannel()`:
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
import { telegramChannel } from "experimental-ash/channels/telegram";
|
|
359
|
-
|
|
360
|
-
export default telegramChannel({
|
|
361
|
-
botUsername: "my_bot",
|
|
362
|
-
});
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
The channel verifies Telegram's `X-Telegram-Bot-Api-Secret-Token` webhook header, accepts updates at
|
|
366
|
-
`/ash/v1/telegram`, dispatches private messages and addressed group messages, and resolves HITL
|
|
367
|
-
inline-keyboard callbacks and ForceReply answers back into Ash input responses. Private chats use
|
|
368
|
-
chat-wide continuation tokens. Group, supergroup, and forum-topic sessions include the chat id,
|
|
369
|
-
optional `message_thread_id`, and a conversation anchor so multiple bot conversations in the same
|
|
370
|
-
chat do not collapse.
|
|
371
|
-
|
|
372
|
-
Default delivery sends plain text through Telegram `sendMessage` with no `parse_mode`, splits text
|
|
373
|
-
at Telegram's 4096-character limit, and uses best-effort `sendChatAction("typing")` progress
|
|
374
|
-
indicators. Photos and documents are exposed as file parts and fetched through Telegram `getFile`
|
|
375
|
-
when the model needs the bytes.
|
|
376
|
-
|
|
377
|
-
See [Telegram channel setup](./telegram.mdx) for webhook registration, environment variables, group
|
|
378
|
-
privacy notes, HITL behavior, attachments, and proactive sessions.
|
|
379
|
-
|
|
380
|
-
## Microsoft Teams Channels
|
|
381
|
-
|
|
382
|
-
Teams channels are authored with `teamsChannel()`:
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
import { teamsChannel } from "experimental-ash/channels/teams";
|
|
386
|
-
|
|
387
|
-
export default teamsChannel();
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
The channel verifies Bot Connector bearer JWTs, accepts Teams Bot Framework Activity POSTs at
|
|
391
|
-
`/ash/v1/teams`, dispatches personal messages and direct bot mentions, renders HITL prompts as
|
|
392
|
-
Adaptive Cards, and sends agent replies through the Bot Framework Connector REST API. Personal chats
|
|
393
|
-
resume by conversation id; channel and group-chat threads resume by conversation id plus root
|
|
394
|
-
activity id.
|
|
395
|
-
|
|
396
|
-
Proactive `receive(teams, { target })` sessions require an existing conversation reference
|
|
397
|
-
(`serviceUrl` and `conversationId`). See [Microsoft Teams channel setup](./teams.mdx) for Azure Bot
|
|
398
|
-
setup, environment variables, proactive handoff, HITL, and file options.
|
|
399
|
-
|
|
400
|
-
## Cross-Channel Hand-off
|
|
401
|
-
|
|
402
|
-
Route handlers can start a session on a different channel via `args.receive(channel, ...)`.
|
|
403
|
-
Use this when an inbound request on one channel should pivot the conversation onto another
|
|
404
|
-
-- for example, an incident webhook hits an HTTP route and the operator interacts in Slack.
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
import { defineChannel, POST } from "experimental-ash/channels";
|
|
408
|
-
import slack from "./slack.js";
|
|
409
|
-
|
|
410
|
-
export default defineChannel({
|
|
411
|
-
routes: [
|
|
412
|
-
POST("/incident", async (req, args) => {
|
|
413
|
-
const incident = await req.json();
|
|
414
|
-
args.waitUntil(
|
|
415
|
-
args.receive(slack, {
|
|
416
|
-
message: `Investigate ${incident.reference}: ${incident.title}`,
|
|
417
|
-
target: { channelId: "C0123ABC" },
|
|
418
|
-
auth: {
|
|
419
|
-
authenticator: "incidentio",
|
|
420
|
-
principalType: "service",
|
|
421
|
-
principalId: incident.actor.id,
|
|
422
|
-
attributes: { reference: incident.reference, severity: incident.severity },
|
|
423
|
-
},
|
|
424
|
-
}),
|
|
425
|
-
);
|
|
426
|
-
return new Response("ok");
|
|
427
|
-
}),
|
|
428
|
-
],
|
|
429
|
-
});
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Semantics:
|
|
433
|
-
|
|
434
|
-
- The target channel's authored `receive(input, { send })` hook owns the continuation-token
|
|
435
|
-
format and the initial state. Callers supply only `{ message, target, auth }`.
|
|
436
|
-
- `auth` flows through to `session.auth.initiator` so the target's event handlers and the
|
|
437
|
-
agent's tools can read who started the session.
|
|
438
|
-
- Calling `args.receive(...)` does **not** also start a session on the current channel. The
|
|
439
|
-
inbound channel's response is whatever the route handler returns explicitly (e.g.
|
|
440
|
-
`200 OK` to acknowledge the webhook).
|
|
441
|
-
- The first argument is the target channel module's default export -- import it directly
|
|
442
|
-
from `agent/channels/<name>.ts`. Identity is matched by reference.
|
|
443
|
-
|
|
444
|
-
### Slack thread anchoring
|
|
445
|
-
|
|
446
|
-
When a Slack session starts without a `threadTs` (programmatic `args.receive(slack, ...)`,
|
|
447
|
-
schedule fires, etc.), the channel **auto-anchors on the first agent post**: the message
|
|
448
|
-
becomes the thread root, and every subsequent post, typing indicator, and inbound
|
|
449
|
-
`@mention` reply in that thread resumes the same Ash session. Schedule digests, webhook
|
|
450
|
-
hand-offs, and other session-from-nothing flows produce clean Slack threads with no extra
|
|
451
|
-
wiring — the channel passes the new channel-local token to
|
|
452
|
-
`ctx.session.setContinuationToken(...)`, and the runtime re-keys the parked session to
|
|
453
|
-
`slack:<channelId>:<anchored-ts>` so follow-up mentions land back in the same workflow.
|
|
454
|
-
|
|
455
|
-
Pass `initialMessage` when you want a structured anchor card to land _before_ the agent
|
|
456
|
-
runs — useful for "Investigation Thread for INC-42"-style banners that should precede
|
|
457
|
-
the model's first reply:
|
|
458
|
-
|
|
459
|
-
```ts
|
|
460
|
-
import { Card, CardText } from "experimental-ash/channels/slack";
|
|
461
|
-
|
|
462
|
-
await args.receive(slack, {
|
|
463
|
-
message: "Begin investigation",
|
|
464
|
-
target: {
|
|
465
|
-
channelId: "C0123ABC",
|
|
466
|
-
initialMessage: {
|
|
467
|
-
card: Card({ children: [CardText("Investigation Thread for INC-42")] }),
|
|
468
|
-
fallbackText: "Investigation Thread for INC-42",
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
auth,
|
|
472
|
-
});
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
For inbound mentions in an existing Slack thread, `onAppMention` and
|
|
476
|
-
`onDirectMessage` may return `context` to inject thread history as
|
|
477
|
-
user messages in session history. See
|
|
478
|
-
[Slack thread context](/docs/channels/slack#thread-context).
|
|
479
|
-
|
|
480
|
-
`threadTs` and `initialMessage` are mutually exclusive: pass `threadTs` to join an existing
|
|
481
|
-
thread, or `initialMessage` to anchor a new one. With neither, the first agent post anchors
|
|
482
|
-
the thread automatically.
|
|
483
|
-
|
|
484
|
-
## Channel Metadata
|
|
485
|
-
|
|
486
|
-
Channels can project a subset of their adapter state as **metadata** available to
|
|
487
|
-
instrumentation resolvers, dynamic tool resolvers, and dynamic skill/instruction resolvers.
|
|
488
|
-
Define a `metadata(state)` function on the channel config:
|
|
489
|
-
|
|
490
|
-
```ts
|
|
491
|
-
import { defineChannel, POST } from "experimental-ash/channels";
|
|
492
|
-
|
|
493
|
-
export default defineChannel({
|
|
494
|
-
state: {
|
|
495
|
-
topic: null as string | null,
|
|
496
|
-
contextMessages: [] as string[],
|
|
497
|
-
internalCounter: 0,
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
metadata(state) {
|
|
501
|
-
// Curated projection — internalCounter is withheld.
|
|
502
|
-
return {
|
|
503
|
-
topic: state.topic,
|
|
504
|
-
contextMessages: state.contextMessages,
|
|
505
|
-
};
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
routes: [
|
|
509
|
-
POST("/start", async (req, { send }) => {
|
|
510
|
-
const body = await req.json();
|
|
511
|
-
await send(body.message, {
|
|
512
|
-
auth: null,
|
|
513
|
-
continuationToken: body.token,
|
|
514
|
-
state: { topic: body.topic, contextMessages: body.context, internalCounter: 0 },
|
|
515
|
-
});
|
|
516
|
-
return new Response("ok");
|
|
517
|
-
}),
|
|
518
|
-
],
|
|
519
|
-
events: {
|
|
520
|
-
"turn.started"(_event, ctx) {
|
|
521
|
-
ctx.state.internalCounter += 1;
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
});
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
The projection is re-evaluated whenever adapter state changes (after channel event handlers
|
|
528
|
-
run). Dynamic tool resolvers read it via `ctx.channel.metadata` and narrow it with
|
|
529
|
-
`isChannel`. See [Dynamic Tools — Channel Metadata](../tools.mdx#channel-metadata) for the
|
|
530
|
-
full consumption pattern.
|
|
531
|
-
|
|
532
|
-
### Subagent propagation
|
|
533
|
-
|
|
534
|
-
When a parent agent dispatches a subagent, the framework forwards the parent's channel
|
|
535
|
-
metadata projection to the child. The subagent's dynamic tool resolvers see the originating
|
|
536
|
-
channel's `kind` and `metadata` — not `kind: "subagent"` with empty metadata. This lets
|
|
537
|
-
subagents conditionally resolve tools based on which channel is driving the conversation.
|
|
538
|
-
|
|
539
|
-
### Shared with instrumentation
|
|
540
|
-
|
|
541
|
-
The same `metadata(state)` projector serves both dynamic resolvers and the instrumentation
|
|
542
|
-
`metadata["step.started"]` resolver. Each consumer picks what it needs: instrumentation
|
|
543
|
-
flattens to `Record<string, string>` for OTel span attributes; tool resolvers use the full
|
|
544
|
-
structured object.
|
|
545
|
-
|
|
546
|
-
## Continuation Tokens
|
|
547
|
-
|
|
548
|
-
Each call to `send(message, { auth, continuationToken, state? })` from a channel route
|
|
549
|
-
addresses a session by its **channel-local raw token**. The framework prepends the
|
|
550
|
-
channel name (the file stem under `agent/channels/`) before handing the token to the
|
|
551
|
-
runtime, so a Slack route that passes `"C0123ABC:1800000000.001234"` ends up addressing
|
|
552
|
-
session `"slack:C0123ABC:1800000000.001234"`.
|
|
553
|
-
|
|
554
|
-
Authored channels typically ship a small helper for building the token:
|
|
555
|
-
|
|
556
|
-
```ts
|
|
557
|
-
import { slackContinuationToken } from "experimental-ash/channels/slack";
|
|
558
|
-
import { twilioContinuationToken } from "experimental-ash/channels/twilio";
|
|
559
|
-
|
|
560
|
-
slackContinuationToken("C0123ABC", "1800000000.001234"); // "C0123ABC:1800000000.001234"
|
|
561
|
-
twilioContinuationToken("+15551234567", "+15557654321"); // "+15551234567:+15557654321"
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
Custom channels just write a function that joins their identity fields. The framework
|
|
565
|
-
does not derive anything for you — the channel owns its token format.
|
|
566
|
-
|
|
567
|
-
### Re-keying mid-session
|
|
568
|
-
|
|
569
|
-
When the identity that should address a session isn't known until the first agent post
|
|
570
|
-
(Slack's auto-anchor: the post's `ts` becomes the thread root), the channel re-keys the
|
|
571
|
-
parked session by calling `ctx.session.setContinuationToken(...)` from a handler. Pass
|
|
572
|
-
the **channel-local raw token**; the runtime preserves the current channel namespace:
|
|
573
|
-
|
|
574
|
-
```ts
|
|
575
|
-
import { defineChannel } from "experimental-ash/channels";
|
|
576
|
-
|
|
577
|
-
defineChannel<{ ref: string | null }>({
|
|
578
|
-
state: { ref: null },
|
|
579
|
-
context(state, session) {
|
|
580
|
-
return {
|
|
581
|
-
state,
|
|
582
|
-
registerAnchor(ref: string) {
|
|
583
|
-
state.ref = ref;
|
|
584
|
-
session.setContinuationToken(ref);
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
},
|
|
588
|
-
events: {
|
|
589
|
-
"message.completed"(_event, ctx) {
|
|
590
|
-
if (!ctx.state.ref) ctx.registerAnchor(mintRef());
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
|
-
routes: [
|
|
594
|
-
/* ... */
|
|
595
|
-
],
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
The workflow runtime disposes the current park hook at the next step boundary and
|
|
600
|
-
registers a new one at the new token. Inbound deliveries already addressed to the old
|
|
601
|
-
token are dropped — coordinate with your senders so follow-up traffic uses the new
|
|
602
|
-
token.
|
|
603
|
-
|
|
604
|
-
## File Uploads
|
|
605
|
-
|
|
606
|
-
`send()` accepts `string | UserContent`. To include file attachments,
|
|
607
|
-
pass a `UserContent` array mixing text and file parts:
|
|
608
|
-
|
|
609
|
-
```ts
|
|
610
|
-
await send(
|
|
611
|
-
[
|
|
612
|
-
{ type: "text", text: body.message },
|
|
613
|
-
{ type: "file", data: imageBytes, mediaType: "image/png" },
|
|
614
|
-
],
|
|
615
|
-
{ auth: null, continuationToken: token },
|
|
616
|
-
);
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
For platforms like Slack where files are behind authenticated URLs,
|
|
620
|
-
put `URL` objects in `FilePart.data` and declare `fetchFile` on the
|
|
621
|
-
channel config:
|
|
622
|
-
|
|
623
|
-
```ts
|
|
624
|
-
defineChannel({
|
|
625
|
-
fetchFile(url) {
|
|
626
|
-
if (!url.startsWith("https://files.slack.com/")) return null;
|
|
627
|
-
return fetch(url, { headers: { authorization: `Bearer ${token}` } })
|
|
628
|
-
.then((r) => r.arrayBuffer())
|
|
629
|
-
.then((b) => ({ bytes: Buffer.from(b) }));
|
|
630
|
-
},
|
|
631
|
-
|
|
632
|
-
routes: [
|
|
633
|
-
POST("/webhook", async (req, { send }) => {
|
|
634
|
-
await send(
|
|
635
|
-
[
|
|
636
|
-
{ type: "text", text: message.text },
|
|
637
|
-
...message.attachments.map((a) => ({
|
|
638
|
-
type: "file" as const,
|
|
639
|
-
data: new URL(a.url),
|
|
640
|
-
mediaType: a.mediaType,
|
|
641
|
-
})),
|
|
642
|
-
],
|
|
643
|
-
{ auth, continuationToken, state },
|
|
644
|
-
);
|
|
645
|
-
}),
|
|
646
|
-
],
|
|
647
|
-
});
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
See [Channel file uploads](./attachments.md) for the full guide.
|
|
651
|
-
|
|
652
|
-
## What To Read Next
|
|
653
|
-
|
|
654
|
-
- [Slack channel setup](./slack.mdx)
|
|
655
|
-
- [GitHub channel setup](./github.md)
|
|
656
|
-
- [Telegram channel setup](./telegram.mdx)
|
|
657
|
-
- [Channel file uploads](./attachments.md)
|
|
658
|
-
- [Project Layout](../project-layout.md)
|
|
659
|
-
- [TypeScript API](../typescript-api.md)
|
|
660
|
-
- [Auth And Route Protection](../auth-and-route-protection.mdx)
|
|
661
|
-
- [Ash External Agent Protocol](../../external-agent-protocol.md)
|