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.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/docs/public/advanced/auth-and-route-protection.mdx +8 -6
  3. package/dist/docs/public/agent-ts.md +17 -47
  4. package/dist/docs/public/channels/README.md +60 -0
  5. package/dist/docs/public/channels/ash.mdx +103 -0
  6. package/dist/docs/public/channels/custom.mdx +288 -0
  7. package/dist/docs/public/channels/discord.mdx +1 -3
  8. package/dist/docs/public/channels/github.md +1 -1
  9. package/dist/docs/public/channels/meta.json +3 -0
  10. package/dist/docs/public/channels/slack.mdx +29 -1
  11. package/dist/docs/public/channels/teams.mdx +1 -3
  12. package/dist/docs/public/channels/telegram.mdx +1 -3
  13. package/dist/docs/public/channels/twilio.mdx +1 -3
  14. package/dist/docs/public/frontend/nextjs.md +24 -8
  15. package/dist/docs/public/onboarding.md +4 -3
  16. package/dist/docs/public/schedules.mdx +4 -4
  17. package/dist/docs/public/tools.mdx +1 -1
  18. package/dist/src/channel/compiled-channel.d.ts +2 -6
  19. package/dist/src/channel/routes.d.ts +75 -7
  20. package/dist/src/channel/routes.js +1 -1
  21. package/dist/src/compiler/manifest.d.ts +4 -4
  22. package/dist/src/compiler/manifest.js +1 -1
  23. package/dist/src/execution/dispatch-code-mode-runtime-actions-step.d.ts +21 -0
  24. package/dist/src/execution/dispatch-code-mode-runtime-actions-step.js +1 -0
  25. package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
  26. package/dist/src/execution/next-driver-action.d.ts +5 -0
  27. package/dist/src/execution/node-step.js +1 -1
  28. package/dist/src/execution/turn-workflow.js +1 -1
  29. package/dist/src/execution/workflow-entry.js +1 -1
  30. package/dist/src/execution/workflow-steps.d.ts +5 -0
  31. package/dist/src/execution/workflow-steps.js +1 -1
  32. package/dist/src/harness/code-mode-runtime-action-state.d.ts +6 -0
  33. package/dist/src/harness/code-mode-runtime-action-state.js +1 -0
  34. package/dist/src/harness/code-mode.js +1 -1
  35. package/dist/src/harness/runtime-actions.d.ts +1 -0
  36. package/dist/src/harness/runtime-actions.js +1 -1
  37. package/dist/src/harness/tool-loop.js +1 -1
  38. package/dist/src/internal/application/package.js +1 -1
  39. package/dist/src/internal/nitro/host/channel-routes.d.ts +2 -2
  40. package/dist/src/internal/nitro/host/channel-routes.js +2 -1
  41. package/dist/src/internal/nitro/host/create-application-nitro.js +1 -1
  42. package/dist/src/internal/nitro/routes/channel-dispatch.d.ts +2 -0
  43. package/dist/src/internal/nitro/routes/channel-dispatch.js +1 -1
  44. package/dist/src/internal/vercel-agent-summary.d.ts +2 -2
  45. package/dist/src/internal/vercel-agent-summary.js +1 -1
  46. package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
  47. package/dist/src/public/channels/index.d.ts +1 -1
  48. package/dist/src/public/channels/index.js +1 -1
  49. package/dist/src/public/definitions/channel.d.ts +7 -0
  50. package/dist/src/public/definitions/defineChannel.d.ts +3 -6
  51. package/dist/src/public/definitions/defineChannel.js +1 -1
  52. package/dist/src/runtime/framework-channels/index.js +1 -1
  53. package/dist/src/runtime/resolve-channel.js +1 -1
  54. package/dist/src/runtime/types.d.ts +8 -3
  55. package/package.json +1 -1
  56. package/dist/docs/public/channels/attachments.md +0 -71
  57. package/dist/docs/public/channels/index.md +0 -661
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.61.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2e3746f: Add WebSocket channel routes via `WS()`, including compiler manifest support, Nitro websocket mounting, and runtime dispatch for custom channel integrations.
8
+
9
+ ## 0.60.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 72d63f2: Make runtime action tools (built-in `agent`, declared subagents, remote agents) callable from code mode. When the agent writes `await agent({ message: "..." })` in sandbox code, the QuickJS sandbox interrupts via the existing code mode interrupt mechanism, the harness parks with a `PendingRuntimeActionBatch` for child dispatch, and the sandbox replays with the child's result on resume.
14
+
3
15
  ## 0.59.0
4
16
 
5
17
  ### Minor Changes
@@ -4,7 +4,9 @@ description: "Protect agent routes with HTTP Basic, JWT, OIDC, and Vercel OIDC."
4
4
  url: /auth-and-route-protection
5
5
  ---
6
6
 
7
- Ash protects its own HTTP routes through the channel layer.
7
+ Ash protects its own HTTP routes through the channel layer. The [Ash channel](/docs/channels/ash)
8
+ is enabled by default and exposes the session API used by local tooling, frontend clients, and other
9
+ HTTP API callers.
8
10
 
9
11
  Route auth and IP policy are not configured in `agent.ts` anymore. They live on the HTTP channel
10
12
  factories and helper functions instead.
@@ -30,8 +32,8 @@ These settings apply to:
30
32
  ## Generated Web Chat Auth
31
33
 
32
34
  `pnpm create experimental-ash-agent` scaffolds `agent/channels/ash.ts` from the Web Chat example.
33
- It permits Vercel OIDC and localhost requests and leaves end-user production auth as an explicit
34
- placeholder:
35
+ Creating this file overrides the default Ash channel settings. The generated version permits Vercel
36
+ OIDC and localhost requests and leaves end-user production auth as an explicit placeholder:
35
37
 
36
38
  ```ts
37
39
  // agent/channels/ash.ts
@@ -250,6 +252,6 @@ export default ashChannel({
250
252
 
251
253
  ## What To Read Next
252
254
 
253
- - [`agent.ts`](./agent-ts.md)
254
- - [Session Context](./session-context.md)
255
- - [Vercel Deployment](./vercel-deployment.md)
255
+ - [`agent.ts`](/docs/agent-ts)
256
+ - [Session Context](/docs/session-context)
257
+ - [Vercel Deployment](/docs/vercel-deployment)
@@ -3,11 +3,11 @@ title: "agent.ts"
3
3
  description: "Configure your agent with agent.ts: model, metadata, and runtime options."
4
4
  ---
5
5
 
6
- `agent.ts` is the additive runtime config entrypoint for an Ash agent.
6
+ `agent.ts` configures the runtime for an Ash agent: model, metadata, build options, and compaction.
7
+ For the agent's behavior and surfaces, see [instructions](./advanced/context-control.md),
8
+ [skills](./skills.md), [tools](./tools.mdx), and [channels](./channels/README.md).
7
9
 
8
- Put instructions in `instructions.md`. Put runtime settings in `agent.ts`.
9
-
10
- ## The Main API
10
+ ## Define An Agent
11
11
 
12
12
  ```ts
13
13
  import { defineAgent } from "experimental-ash";
@@ -27,25 +27,6 @@ export default defineAgent({
27
27
  });
28
28
  ```
29
29
 
30
- ## What Belongs Here
31
-
32
- Use `agent.ts` for:
33
-
34
- - model selection
35
- - metadata you want preserved in runtime traces
36
- - hosted-build packaging controls
37
- - compaction settings
38
- - provider-specific model options
39
-
40
- Per-tool approval (formerly "human-in-the-loop policy") lives on each tool via `needsApproval`,
41
- not in `agent.ts`. See [Human In The Loop](./human-in-the-loop.mdx).
42
-
43
- For OpenTelemetry configuration, use `instrumentation.ts` instead. See
44
- [`instrumentation.ts`](./instrumentation.md).
45
-
46
- Do not use `agent.ts` for long-form instructions. Put those in `instructions.md` (or `instructions.ts`) and
47
- `skills/`.
48
-
49
30
  ## `model`
50
31
 
51
32
  Ash accepts either:
@@ -130,27 +111,7 @@ server output instead of being bundled.
130
111
  - `model` optionally overrides the model used for compaction summaries
131
112
 
132
113
  The shared per-run workspace is configured on the sandbox, not on `agent.ts`. See
133
- [Workspace](./workspace.md) and [Sandboxes](./sandbox.md).
134
-
135
- ## Human In The Loop
136
-
137
- Approval policy lives on individual tools via `needsApproval` in `defineTool({...})`, not on
138
- `agent.ts`. See [Human In The Loop](./human-in-the-loop.mdx) for the runtime flow, `input.requested`
139
- events, and HTTP response payloads.
140
-
141
- ## Telemetry
142
-
143
- OpenTelemetry tracing is configured in `agent/instrumentation.ts`, not in `agent.ts`. See
144
- [`instrumentation.ts`](./instrumentation.md) for setup.
145
-
146
- ## Route Auth And Network Policy
147
-
148
- `agent.ts` does not define inbound auth or IP policy anymore.
149
-
150
- Those concerns live on the HTTP channel layer instead. See:
151
-
152
- - [Auth And Route Protection](./auth-and-route-protection.mdx)
153
- - [Channels](./channels/README.md)
114
+ [Workspace](./advanced/workspace.md) and [Sandboxes](./sandbox.md).
154
115
 
155
116
  ## A Good Default
156
117
 
@@ -166,9 +127,18 @@ export default defineAgent({
166
127
 
167
128
  That is enough to start. Add build and compaction settings only when you need them.
168
129
 
130
+ ## Where Other Settings Live
131
+
132
+ A few adjacent concerns live in dedicated files:
133
+
134
+ - Long-form instructions → [`instructions.md`](./advanced/context-control.md)
135
+ - Per-tool approval → [Human In The Loop](./human-in-the-loop.mdx)
136
+ - OpenTelemetry → [`instrumentation.ts`](./advanced/instrumentation.md)
137
+ - Inbound auth → [Auth And Route Protection](./advanced/auth-and-route-protection.mdx)
138
+
169
139
  ## What To Read Next
170
140
 
171
- - [Context Control](./context-control.md)
141
+ - [Context Control](./advanced/context-control.md)
172
142
  - [Human In The Loop](./human-in-the-loop.mdx)
173
- - [Workspace](./workspace.md)
174
- - [Auth And Route Protection](./auth-and-route-protection.mdx)
143
+ - [Workspace](./advanced/workspace.md)
144
+ - [Auth And Route Protection](./advanced/auth-and-route-protection.mdx)
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: "Overview"
3
+ description: "Deliver your agent over HTTP, Slack, Discord, GitHub, 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. Each channel owns the platform-specific delivery details, while the runtime and
9
+ harness still own model turns, tool execution, compaction, and session persistence.
10
+
11
+ Most apps start with the Ash channel for web and local clients, then add platform channels when the
12
+ agent should live inside a collaboration or messaging surface.
13
+
14
+ ## Define a channel
15
+
16
+ Create a channel file under `agent/channels/` in the root agent:
17
+
18
+ ```text
19
+ agent/
20
+ agent.ts
21
+ channels/
22
+ slack.ts
23
+ github.ts
24
+ internal-webhook.ts
25
+ ```
26
+
27
+ The file stem becomes the channel id. For example, `agent/channels/internal-webhook.ts` is addressed
28
+ as `internal-webhook`.
29
+
30
+ ## Built-in channels
31
+
32
+ - [Ash](/docs/channels/ash) - the default HTTP session protocol used by the terminal UI, Ash client
33
+ integrations, and deployed web frontends.
34
+ - [Discord](/docs/channels/discord) - Discord HTTP Interactions for slash commands, components, and modals.
35
+ - [Slack](/docs/channels/slack) - Slack app mentions, direct messages, message delivery, typing indicators,
36
+ and HITL interactions.
37
+ - [GitHub](/docs/channels/github) - GitHub App webhooks, PR context, issue and review comments,
38
+ and sandbox checkout.
39
+ - [Microsoft Teams](/docs/channels/teams) - Bot Framework Activity webhooks, Adaptive Cards, and Teams
40
+ replies.
41
+ - [Telegram](/docs/channels/telegram) - Telegram Bot API webhooks, replies, inline keyboards, and group
42
+ messages.
43
+ - [Twilio](/docs/channels/twilio) - inbound SMS and speech-transcribed phone calls.
44
+
45
+ ## Need another surface?
46
+
47
+ Use [Custom channels](/docs/channels/custom) when you need to expose your own HTTP or WebSocket
48
+ endpoints, adapt an internal webhook, or connect a platform that does not have a first-class channel
49
+ yet.
50
+
51
+ ## What to read next
52
+
53
+ - [Ash channel](/docs/channels/ash)
54
+ - [Slack channel](/docs/channels/slack)
55
+ - [GitHub channel](/docs/channels/github)
56
+ - [Telegram channel](/docs/channels/telegram)
57
+ - [Custom channels](/docs/channels/custom)
58
+ - [Project Layout](/docs/project-layout)
59
+ - [TypeScript API](/docs/typescript-api)
60
+ - [Auth And Route Protection](/docs/auth-and-route-protection)
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: "Ash"
3
+ description: "Expose Ash's default HTTP API for local tools, frontends, and SDK clients."
4
+ ---
5
+
6
+ The Ash channel exposes the framework's default HTTP API for an agent. Use it when something should
7
+ talk to your agent over HTTP: local tooling, a [frontend client](/docs/frontend), or another
8
+ API client that needs to start sessions, send user messages, and stream agent events.
9
+
10
+ `ashChannel()` mounts Ash's canonical session routes under `/ash/v1/session*`. These routes are
11
+ enabled by default, even when `agent/channels/ash.ts` does not exist.
12
+
13
+ ## When to use it
14
+
15
+ - You want HTTP/API access to your agent.
16
+ - You are using the terminal UI.
17
+ - You are building a frontend that talks to the agent through [`useAshAgent()`](/docs/frontend/use-ash-agent)
18
+ or Ash's HTTP session API.
19
+ - You want to choose the route authentication policy for the default session API.
20
+
21
+ Most apps do not need much code here. Add `agent/channels/ash.ts` when you want to override the
22
+ default Ash channel settings:
23
+
24
+ ```ts
25
+ import { ashChannel } from "experimental-ash/channels/ash";
26
+ import { localDev, vercelOidc } from "experimental-ash/channels/auth";
27
+
28
+ export default ashChannel({
29
+ auth: [localDev(), vercelOidc()],
30
+ });
31
+ ```
32
+
33
+ ## Exposed routes
34
+
35
+ The Ash channel exposes the HTTP routes used to create sessions, send follow-up messages, and stream
36
+ events from a running session:
37
+
38
+ - `GET /ash/v1/health`
39
+ - `POST /ash/v1/session`
40
+ - `POST /ash/v1/session/:sessionId`
41
+ - `GET /ash/v1/session/:sessionId/stream`
42
+
43
+ See [Runs and Streaming](/docs/runs-and-streaming) for the route-level flow.
44
+
45
+ ## Authentication
46
+
47
+ The `auth` option controls who can call the Ash HTTP routes. If a browser app, mobile app, or other
48
+ client connects directly to the Ash API, add your own production authentication.
49
+
50
+ The default helpers are meant for development and trusted infrastructure:
51
+
52
+ - `localDev()` accepts requests during local development.
53
+ - `vercelOidc()` lets the locally installed CLI talk to a deployed agent and lets other internal
54
+ Vercel deployments from your team call it.
55
+
56
+ Those defaults do not give browser users or external clients protected production access. For that,
57
+ wire the Ash channel to your app's auth system, such as Clerk, Auth.js, or your own OIDC/JWT
58
+ verification.
59
+
60
+ `pnpm create experimental-ash-agent` scaffolds an example `agent/channels/ash.ts` with a production
61
+ auth placeholder so you can replace it before exposing the API to real users.
62
+
63
+ For the full auth model and helper list, see
64
+ [Auth and Route Protection](/docs/auth-and-route-protection).
65
+
66
+ ## Advanced customization
67
+
68
+ Use `onMessage` to add request-specific context before the agent sees the user message, and `events`
69
+ to observe stream events from sessions created through the Ash channel.
70
+
71
+ ```ts
72
+ import { ashChannel, defaultAshAuth } from "experimental-ash/channels/ash";
73
+ import { localDev, vercelOidc } from "experimental-ash/channels/auth";
74
+
75
+ export default ashChannel({
76
+ auth: [localDev(), vercelOidc()],
77
+ onMessage(ctx, message) {
78
+ const callerId = ctx.ash.caller?.principalId ?? "anonymous";
79
+
80
+ return {
81
+ auth: defaultAshAuth(ctx),
82
+ context: [`HTTP caller ${callerId} sent: ${message}`],
83
+ };
84
+ },
85
+ events: {
86
+ "message.completed"(event, channel, ctx) {
87
+ console.log("Ash response completed", {
88
+ continuationToken: channel.continuationToken,
89
+ message: event.message,
90
+ sessionId: ctx.session.id,
91
+ });
92
+ },
93
+ },
94
+ });
95
+ ```
96
+
97
+ ## Frontend clients
98
+
99
+ Frontend docs cover the client side of this API:
100
+
101
+ - [Frontend overview](/docs/frontend) explains the web integration options.
102
+ - [`useAshAgent`](/docs/frontend/use-ash-agent) shows the React hook that calls the Ash channel from
103
+ browser UI.
@@ -0,0 +1,288 @@
1
+ ---
2
+ title: "Custom channels"
3
+ description: "Author custom HTTP and WebSocket channels with routes, events, metadata, continuation tokens, and file uploads."
4
+ ---
5
+
6
+ Custom channels are for surfaces Ash does not ship as first-class integrations. They let you expose
7
+ HTTP or WebSocket endpoints, parse incoming requests, start or resume sessions, observe runtime
8
+ events, and own delivery back to your platform.
9
+
10
+ ## File location and identity
11
+
12
+ Custom channels live in `agent/channels/` at the root agent level. Local subagents do not declare
13
+ channels today.
14
+
15
+ The channel file stem becomes the channel id, so `agent/channels/internal-webhook.ts` is addressed as
16
+ `internal-webhook`. Channel files are module-backed; export the channel definition as the default
17
+ export.
18
+
19
+ ## Main API
20
+
21
+ ```ts
22
+ import { defineChannel, GET, POST } from "experimental-ash/channels";
23
+
24
+ export default defineChannel({
25
+ routes: [
26
+ POST("/message", async (req, { send }) => {
27
+ const body = await req.json();
28
+ const session = await send(body.message, {
29
+ auth: null,
30
+ continuationToken: body.token,
31
+ });
32
+
33
+ return Response.json({ sessionId: session.id });
34
+ }),
35
+ GET("/sessions/:sessionId/stream", async (_req, { getSession, params }) => {
36
+ const session = getSession(params.sessionId);
37
+ const stream = await session.getEventStream();
38
+
39
+ return new Response(stream, {
40
+ headers: { "content-type": "text/event-stream" },
41
+ });
42
+ }),
43
+ ],
44
+ events: {
45
+ "message.completed"(event, channel, ctx) {
46
+ // deliver completed messages back to the surface that owns this channel
47
+ },
48
+ },
49
+ });
50
+ ```
51
+
52
+ `defineChannel({ routes, ... })` defines a channel. Routes are declared with `POST()` and `GET()`
53
+ helpers. Each route handler receives the raw `Request` and a helpers object:
54
+
55
+ - `send(message, { auth, continuationToken, state? })` - start or resume a session. Returns a
56
+ `Session`.
57
+ - `getSession(sessionId)` - look up an existing session. The returned `Session` exposes
58
+ `getEventStream({ startIndex? })` for streaming.
59
+ - `params` - route parameters extracted from the path pattern.
60
+ - `waitUntil(promise)` - extend the request lifetime for background work.
61
+
62
+ Event handlers like `"message.completed"` are declared under the `events` key on the config object.
63
+ They receive `(eventData, channel, ctx)` where `channel` carries platform handles and session
64
+ continuation operations, and `ctx` is the Ash `SessionContext`.
65
+
66
+ ## WebSocket routes
67
+
68
+ Use `WS()` when a custom channel needs a WebSocket endpoint. The route handler runs once per upgrade
69
+ request and returns lifecycle hooks for that connection:
70
+
71
+ ```ts
72
+ import { defineChannel, WS } from "experimental-ash/channels";
73
+
74
+ export default defineChannel({
75
+ routes: [
76
+ WS("/voice/ws", async (_req, { send }) => ({
77
+ async message(_peer, message) {
78
+ await send(message.text(), {
79
+ auth: null,
80
+ continuationToken: "voice-demo",
81
+ });
82
+ },
83
+ })),
84
+ ],
85
+ });
86
+ ```
87
+
88
+ `WS()` handlers receive the same helpers as HTTP route handlers: `send`, `getSession`, `receive`,
89
+ `params`, `waitUntil`, and `requestIp`. The returned hooks are Ash-owned structural types compatible
90
+ with Nitro/H3 websocket routing, including `upgrade`, `open`, `message`, `close`, and `error`.
91
+
92
+ ## Cross-channel hand-off
93
+
94
+ Route handlers can start a session on a different channel via `args.receive(channel, ...)`. Use this
95
+ when an inbound request on one channel should pivot the conversation onto another, such as an
96
+ incident webhook that should open an investigation thread in Slack.
97
+
98
+ ```ts
99
+ import { defineChannel, POST } from "experimental-ash/channels";
100
+ import slack from "./slack.js";
101
+
102
+ export default defineChannel({
103
+ routes: [
104
+ POST("/incident", async (req, args) => {
105
+ const incident = await req.json();
106
+
107
+ args.waitUntil(
108
+ args.receive(slack, {
109
+ message: `Investigate ${incident.reference}: ${incident.title}`,
110
+ target: { channelId: "C0123ABC" },
111
+ auth: {
112
+ authenticator: "incidentio",
113
+ principalType: "service",
114
+ principalId: incident.actor.id,
115
+ attributes: { reference: incident.reference, severity: incident.severity },
116
+ },
117
+ }),
118
+ );
119
+
120
+ return new Response("ok");
121
+ }),
122
+ ],
123
+ });
124
+ ```
125
+
126
+ Semantics:
127
+
128
+ - The target channel's authored `receive(input, { send })` hook owns the continuation-token format
129
+ and initial state. Callers supply only `{ message, target, auth }`.
130
+ - `auth` flows through to `session.auth.initiator` so the target's event handlers and the agent's
131
+ tools can read who started the session.
132
+ - Calling `args.receive(...)` does not also start a session on the current channel. The inbound
133
+ channel's response is whatever the route handler returns explicitly.
134
+ - The first argument is the target channel module's default export. Import it directly from
135
+ `agent/channels/<name>.ts`. Identity is matched by reference.
136
+
137
+ ## Channel metadata
138
+
139
+ Channels can project a subset of their adapter state as metadata available to instrumentation
140
+ resolvers, dynamic tool resolvers, and dynamic skill/instruction resolvers. Define a
141
+ `metadata(state)` function on the channel config:
142
+
143
+ ```ts
144
+ import { defineChannel, POST } from "experimental-ash/channels";
145
+
146
+ export default defineChannel({
147
+ state: {
148
+ topic: null as string | null,
149
+ contextMessages: [] as string[],
150
+ internalCounter: 0,
151
+ },
152
+
153
+ metadata(state) {
154
+ return {
155
+ topic: state.topic,
156
+ contextMessages: state.contextMessages,
157
+ };
158
+ },
159
+
160
+ routes: [
161
+ POST("/start", async (req, { send }) => {
162
+ const body = await req.json();
163
+ await send(body.message, {
164
+ auth: null,
165
+ continuationToken: body.token,
166
+ state: { topic: body.topic, contextMessages: body.context, internalCounter: 0 },
167
+ });
168
+
169
+ return new Response("ok");
170
+ }),
171
+ ],
172
+ events: {
173
+ "turn.started"(_event, ctx) {
174
+ ctx.state.internalCounter += 1;
175
+ },
176
+ },
177
+ });
178
+ ```
179
+
180
+ The projection is re-evaluated whenever adapter state changes after channel event handlers run.
181
+ Dynamic tool resolvers read it via `ctx.channel.metadata` and narrow it with `isChannel`. See
182
+ [Dynamic Tools - Channel Metadata](/docs/tools#channel-metadata) for the full consumption pattern.
183
+
184
+ When a parent agent dispatches a subagent, the framework forwards the parent's channel metadata
185
+ projection to the child. The same `metadata(state)` projector also serves instrumentation metadata
186
+ resolvers.
187
+
188
+ ## Continuation tokens
189
+
190
+ Each call to `send(message, { auth, continuationToken, state? })` from a channel route addresses a
191
+ session by its channel-local raw token. The framework prepends the channel name, derived from the
192
+ file stem under `agent/channels/`, before handing the token to the runtime.
193
+
194
+ ```ts
195
+ import { slackContinuationToken } from "experimental-ash/channels/slack";
196
+ import { twilioContinuationToken } from "experimental-ash/channels/twilio";
197
+
198
+ slackContinuationToken("C0123ABC", "1800000000.001234"); // "C0123ABC:1800000000.001234"
199
+ twilioContinuationToken("+15551234567", "+15557654321"); // "+15551234567:+15557654321"
200
+ ```
201
+
202
+ Custom channels write their own function that joins the identity fields. The framework does not
203
+ derive anything for you; the channel owns its token format.
204
+
205
+ When the identity that should address a session is not known until later, the channel can re-key the
206
+ parked session by calling `ctx.session.setContinuationToken(...)` from a handler. Pass the
207
+ channel-local raw token; the runtime preserves the current channel namespace.
208
+
209
+ ```ts
210
+ import { defineChannel } from "experimental-ash/channels";
211
+
212
+ defineChannel<{ ref: string | null }>({
213
+ state: { ref: null },
214
+ context(state, session) {
215
+ return {
216
+ state,
217
+ registerAnchor(ref: string) {
218
+ state.ref = ref;
219
+ session.setContinuationToken(ref);
220
+ },
221
+ };
222
+ },
223
+ events: {
224
+ "message.completed"(_event, ctx) {
225
+ if (!ctx.state.ref) ctx.registerAnchor(mintRef());
226
+ },
227
+ },
228
+ routes: [
229
+ /* ... */
230
+ ],
231
+ });
232
+ ```
233
+
234
+ The workflow runtime disposes the current park hook at the next step boundary and registers a new
235
+ one at the new token. Inbound deliveries already addressed to the old token are dropped, so
236
+ coordinate with your senders to use the new token.
237
+
238
+ ## File uploads
239
+
240
+ `send()` accepts `string | UserContent`. To include file attachments, pass a `UserContent` array
241
+ mixing text and file parts:
242
+
243
+ ```ts
244
+ await send(
245
+ [
246
+ { type: "text", text: body.message },
247
+ { type: "file", data: imageBytes, mediaType: "image/png" },
248
+ ],
249
+ { auth, continuationToken },
250
+ );
251
+ ```
252
+
253
+ For platforms like Slack where files are behind authenticated URLs, put a `URL` object in
254
+ `FilePart.data` and declare `fetchFile` on the channel config:
255
+
256
+ ```ts
257
+ defineChannel({
258
+ fetchFile(url) {
259
+ if (!url.startsWith("https://files.slack.com/")) return null;
260
+ return fetch(url, { headers: { authorization: `Bearer ${token}` } })
261
+ .then((r) => r.arrayBuffer())
262
+ .then((b) => ({ bytes: Buffer.from(b) }));
263
+ },
264
+
265
+ routes: [
266
+ POST("/webhook", async (req, { send }) => {
267
+ await send(
268
+ [
269
+ { type: "text", text: message.text },
270
+ ...message.attachments.map((a) => ({
271
+ type: "file" as const,
272
+ data: new URL(a.url),
273
+ mediaType: a.mediaType,
274
+ })),
275
+ ],
276
+ { auth, continuationToken, state },
277
+ );
278
+ }),
279
+ ],
280
+ });
281
+ ```
282
+
283
+ The `URL` object survives the queue boundary as a string and is reconstituted inside the workflow
284
+ step. The staging pipeline calls `fetchFile(url)`: return bytes to stage the file to the sandbox, or
285
+ return `null` to let the URL pass through to the model provider.
286
+
287
+ The framework handles staging bytes to the sandbox, enforcing upload policy, hydrating files for the
288
+ model call, and reconstituting `URL` objects after queue serialization.
@@ -1,10 +1,8 @@
1
1
  ---
2
- title: "Discord channel setup"
2
+ title: "Discord"
3
3
  description: "Create a Discord-backed Ash channel for HTTP Interactions."
4
4
  ---
5
5
 
6
- # Discord Channel Setup
7
-
8
6
  The Discord channel accepts HTTP Interactions: slash/application commands, message components, and
9
7
  modal submissions. It verifies Discord's Ed25519 signature headers before parsing the request,
10
8
  acknowledges commands immediately, and continues Ash work in the background.
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: "GitHub channel setup"
2
+ title: "GitHub"
3
3
  description: "Create a GitHub App-backed Ash channel with automatic PR context and sandbox checkout."
4
4
  type: integration
5
5
  related:
@@ -0,0 +1,3 @@
1
+ {
2
+ "pages": ["README", "ash", "discord", "slack", "github", "teams", "telegram", "twilio", "custom"]
3
+ }
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: "Slack channel setup"
2
+ title: "Slack"
3
3
  description: "Create a Slack-backed Ash channel with Vercel Connect."
4
4
  type: integration
5
5
  related:
@@ -255,6 +255,34 @@ export default slackChannel({
255
255
  `context` strings are appended as user messages to `session.history` before the delivery message.
256
256
  They persist across the session and are visible to the model on all subsequent turns.
257
257
 
258
+ ### Thread Anchoring
259
+
260
+ When a Slack session starts without a `threadTs`, such as from `args.receive(slack, ...)` or a
261
+ schedule, the channel auto-anchors on the first agent post. That message becomes the thread root, and
262
+ subsequent posts, typing indicators, and inbound mentions in that thread resume the same Ash session.
263
+
264
+ Pass `initialMessage` when you want a structured anchor card to land before the agent runs:
265
+
266
+ ```ts
267
+ import { Card, CardText } from "experimental-ash/channels/slack";
268
+
269
+ await args.receive(slack, {
270
+ message: "Begin investigation",
271
+ target: {
272
+ channelId: "C0123ABC",
273
+ initialMessage: {
274
+ card: Card({ children: [CardText("Investigation Thread for INC-42")] }),
275
+ fallbackText: "Investigation Thread for INC-42",
276
+ },
277
+ },
278
+ auth,
279
+ });
280
+ ```
281
+
282
+ `threadTs` and `initialMessage` are mutually exclusive: pass `threadTs` to join an existing thread,
283
+ or `initialMessage` to anchor a new one. With neither, the first agent post anchors the thread
284
+ automatically.
285
+
258
286
  ### Direct Messages
259
287
 
260
288
  Add `onDirectMessage` alongside `onAppMention` to handle 1:1 DMs:
@@ -1,10 +1,8 @@
1
1
  ---
2
- title: "Microsoft Teams channel setup"
2
+ title: "Microsoft Teams"
3
3
  description: "Create a Microsoft Teams-backed Ash channel with the Bot Framework Activity protocol."
4
4
  ---
5
5
 
6
- # Microsoft Teams Channel Setup
7
-
8
6
  The Teams channel accepts Bot Framework Activity POSTs from Microsoft Teams, verifies Bot
9
7
  Connector bearer JWTs, dispatches message activities to Ash, renders HITL prompts as Adaptive
10
8
  Cards, and delivers agent responses through the Bot Framework Connector REST API.
@@ -1,10 +1,8 @@
1
1
  ---
2
- title: "Telegram channel setup"
2
+ title: "Telegram"
3
3
  description: "Create a Telegram-backed Ash channel for bot webhooks, replies, HITL, and attachments."
4
4
  ---
5
5
 
6
- # Telegram Channel Setup
7
-
8
6
  The Telegram channel accepts Telegram Bot API webhooks. It verifies Telegram's
9
7
  `X-Telegram-Bot-Api-Secret-Token` header before parsing the update, dispatches private messages and
10
8
  addressed group messages, and sends default replies through `sendMessage`.