experimental-ash 0.51.0 → 0.52.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 (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/docs/public/advanced/instrumentation.md +87 -119
  3. package/dist/docs/public/advanced/typescript-api.md +4 -2
  4. package/dist/docs/public/channels/discord.mdx +2 -2
  5. package/dist/docs/public/channels/index.md +4 -4
  6. package/dist/docs/public/channels/teams.mdx +1 -1
  7. package/dist/docs/public/channels/telegram.mdx +2 -2
  8. package/dist/docs/public/meta.json +1 -0
  9. package/dist/docs/public/onboarding.md +119 -0
  10. package/dist/docs/public/schedules.mdx +4 -4
  11. package/dist/src/channel/compiled-channel.d.ts +2 -2
  12. package/dist/src/channel/cross-channel-receive.d.ts +6 -6
  13. package/dist/src/channel/cross-channel-receive.js +1 -1
  14. package/dist/src/channel/receive-target.d.ts +17 -0
  15. package/dist/src/cli/commands/channels.d.ts +2 -0
  16. package/dist/src/cli/commands/channels.js +1 -1
  17. package/dist/src/cli/commands/info.d.ts +46 -1
  18. package/dist/src/cli/commands/info.js +2 -2
  19. package/dist/src/cli/run.d.ts +3 -1
  20. package/dist/src/cli/run.js +2 -2
  21. package/dist/src/harness/{instrumentation-metadata.d.ts → instrumentation-runtime-context.d.ts} +2 -2
  22. package/dist/src/harness/instrumentation-runtime-context.js +1 -0
  23. package/dist/src/harness/tool-loop.js +1 -1
  24. package/dist/src/internal/application/package.js +1 -1
  25. package/dist/src/internal/instrumentation.d.ts +8 -7
  26. package/dist/src/internal/instrumentation.js +1 -1
  27. package/dist/src/packages/ash-scaffold/src/channels.js +2 -2
  28. package/dist/src/packages/ash-scaffold/src/human-action.js +1 -0
  29. package/dist/src/packages/ash-scaffold/src/index.js +1 -1
  30. package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +1 -1
  31. package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -1
  32. package/dist/src/public/channels/discord/discordChannel.d.ts +3 -3
  33. package/dist/src/public/channels/discord/discordChannel.js +1 -1
  34. package/dist/src/public/channels/discord/index.d.ts +1 -1
  35. package/dist/src/public/channels/slack/index.d.ts +1 -1
  36. package/dist/src/public/channels/slack/slackChannel.d.ts +4 -3
  37. package/dist/src/public/channels/slack/slackChannel.js +1 -1
  38. package/dist/src/public/channels/teams/index.d.ts +1 -1
  39. package/dist/src/public/channels/teams/teamsChannel.d.ts +3 -3
  40. package/dist/src/public/channels/teams/teamsChannel.js +1 -1
  41. package/dist/src/public/channels/telegram/index.d.ts +1 -1
  42. package/dist/src/public/channels/telegram/telegramChannel.d.ts +3 -3
  43. package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
  44. package/dist/src/public/channels/twilio/index.d.ts +1 -1
  45. package/dist/src/public/channels/twilio/twilioChannel.d.ts +3 -3
  46. package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
  47. package/dist/src/public/definitions/defineChannel.d.ts +8 -8
  48. package/dist/src/public/definitions/schedule.d.ts +2 -2
  49. package/dist/src/public/instrumentation/index.d.ts +21 -11
  50. package/dist/src/public/schedules/index.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/dist/src/channel/receive-args.d.ts +0 -17
  53. package/dist/src/harness/instrumentation-metadata.js +0 -1
  54. /package/dist/src/channel/{receive-args.js → receive-target.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.52.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 56fdb14: `ash info --json` now emits a machine-readable view of the compiled agent (status, channels, tools, model, and message routes), and `ash channels add <kind> --yes` skips Ash confirmations for non-interactive use. Slack setup still runs Vercel Connect's interactive OAuth flow before attaching the connector to the Ash route.
8
+ - d486e9e: Rename cross-channel receive options from `args` to `target`. Channel receive hooks now read `input.target`, and native channel receive target types use the `*ReceiveTarget` naming.
9
+ - b91b550: Breaking: replace `defineInstrumentation({ metadata })` with `defineInstrumentation({ events: { "step.started": () => ({ runtimeContext }) } })` for per-step AI SDK telemetry context.
10
+
3
11
  ## 0.51.0
4
12
 
5
13
  ### Minor Changes
@@ -1,14 +1,28 @@
1
1
  ---
2
2
  title: "instrumentation.ts"
3
- description: "Configure OpenTelemetry exporters and AI SDK span behavior for your agent."
3
+ description: "Configure OpenTelemetry export and per-call AI SDK span context for your agent, and read the workflow run tags Ash emits automatically."
4
4
  url: /instrumentation
5
5
  ---
6
6
 
7
- `instrumentation.ts` is the single place to configure OpenTelemetry for an Ash agent. It controls
8
- both _where_ spans are exported and _what_ the AI SDK records in those spans.
7
+ `instrumentation.ts` is where you configure how an Ash agent is observed. The framework
8
+ auto-discovers `agent/instrumentation.ts` and runs it at server startup before any agent code. Its
9
+ presence implicitly enables telemetry -- there is no separate `isEnabled` toggle.
9
10
 
10
- The framework auto-discovers `agent/instrumentation.ts` and runs it at server startup before any
11
- agent code.
11
+ ## Three observability surfaces
12
+
13
+ Ash observes an agent through three distinct surfaces. They do not all live in this file, and they
14
+ write to different places -- so it helps to keep them apart:
15
+
16
+ | Surface | Configured in `instrumentation.ts`? | What it is |
17
+ | -------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | **Workflow run tags** (`$ash.*`) | No -- automatic | Framework-owned attributes on each Vercel Workflow run. Let dashboards stitch session/turn/subagent runs into a tree and surface model and token usage. |
19
+ | **OpenTelemetry export** | Yes -- `setup`, `recordInputs`, `recordOutputs`, `functionId` | Where AI SDK spans are exported and what they record. |
20
+ | **Runtime context events** | Yes -- `events["step.started"]` | Per-model-call values written into the AI SDK's runtime context, which the AI SDK carries onto its spans. |
21
+
22
+ The two configurable surfaces send AI SDK spans to your OpenTelemetry backend. Workflow run tags are
23
+ a separate system: they live on the Vercel Workflow run and are queryable in the Workflow dashboard,
24
+ not on your OTel spans. The sections below cover what you configure here;
25
+ [Workflow run tags](#workflow-run-tags) documents what Ash emits on its own.
12
26
 
13
27
  ## The Main API
14
28
 
@@ -30,123 +44,67 @@ export default defineInstrumentation({
30
44
  });
31
45
  ```
32
46
 
33
- Export the result of `defineInstrumentation` as the default export. Its presence implicitly enables
34
- telemetry -- there is no separate `isEnabled` toggle.
47
+ Export the result of `defineInstrumentation` as the default export.
35
48
 
36
- ## `setup`
49
+ ## OpenTelemetry
37
50
 
38
51
  The `setup` callback is invoked by the framework at server startup with the resolved agent name. Use
39
52
  it to register your OTel provider (e.g. `registerOTel` from `@vercel/otel`). The
40
- `context.agentName` is derived from the agent's filesystem path at compile time, so you never need
41
- to hard-code a service name.
53
+ `context.agentName` is resolved at compile time from your project -- the package's `name`, falling
54
+ back to the app directory name -- so you never need to hard-code a service name.
42
55
 
43
56
  Any OTel-compatible backend works (Braintrust, Honeycomb, Datadog, Jaeger). Install the exporter
44
57
  package you need and configure it in the callback.
45
58
 
46
- ## AI SDK Span Settings
47
-
48
- These fields control what the AI SDK records inside OTel spans:
59
+ Three more fields control what the AI SDK records inside those spans (see the AI SDK's
60
+ [telemetry reference](https://ai-sdk.dev/docs/ai-sdk-core/telemetry)):
49
61
 
50
62
  - `recordInputs` -- record full message history on each step span (defaults to `true`). Set to
51
- `false` to disable if inputs contain sensitive content or you want to reduce span payload size.
63
+ `false` if inputs contain sensitive content or you want to reduce span payload size.
52
64
  - `recordOutputs` -- record model outputs on spans (defaults to `true`). Set to `false` to disable
53
65
  output recording.
54
- - `functionId` -- override the function name on spans (defaults to the agent name)
55
- - `metadata["step.started"]` -- a synchronous callback that returns key-value pairs for one
56
- model-call attempt
57
-
58
- ## Metadata
59
-
60
- Use `metadata["step.started"]` when the metadata depends on the current session, turn, step,
61
- channel, or model input:
62
-
63
- ```ts
64
- import { defineInstrumentation } from "experimental-ash/instrumentation";
65
-
66
- export default defineInstrumentation({
67
- metadata: {
68
- "step.started"(input) {
69
- if (input.channel.kind !== "channel:support") {
70
- return {};
71
- }
66
+ - `functionId` -- override the function name on spans (defaults to the agent name).
72
67
 
73
- return {
74
- "slack.channel_id": input.channel.metadata.channelId ?? "",
75
- "slack.user_id": input.channel.metadata.triggeringUserId ?? "",
76
- };
77
- },
78
- },
79
- });
80
- ```
81
-
82
- For authored channels, TypeScript gets channel metadata types from a compiler-owned declaration
83
- file. When Ash compiles `agent/channels/support.ts`, it writes
84
- `.ash/compile/channel-instrumentation-types.d.ts` with a `ChannelMetadataMap` entry for the
85
- filename-derived kind and a `ChannelReferenceMap` entry for `isChannel`:
68
+ The third configurable surface, [runtime context events](#runtime-context), attaches per-model-call
69
+ values to these spans.
86
70
 
87
- ```ts
88
- import type { InferChannelMetadata } from "experimental-ash/channels";
89
-
90
- declare module "experimental-ash/instrumentation" {
91
- interface ChannelMetadataMap {
92
- readonly "channel:support": InferChannelMetadata<
93
- typeof import("../../agent/channels/support.js").default
94
- >;
95
- }
96
- interface ChannelReferenceMap {
97
- readonly "channel:support": typeof import("../../agent/channels/support.js").default;
98
- }
99
- }
100
- ```
71
+ ## Runtime Context
101
72
 
102
- Keep `.ash/**/*.d.ts` in your app's `tsconfig.json` `include` list so TypeScript sees that file.
103
- Then either `input.channel.kind === "channel:support"` or `isChannel(input.channel, supportChannel)`
104
- narrows `input.channel.metadata` to the return type of the channel's `metadata(state)` function:
73
+ _Runtime context_ is an [AI SDK concept](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text):
74
+ a user-defined object that flows through a generation lifecycle. Ash exposes it through
75
+ `events["step.started"]`, a callback that runs once Ash has assembled the model input for an attempt
76
+ and returns `{ runtimeContext }`. Because Ash registers the AI SDK's OpenTelemetry integration with runtime
77
+ context enabled, those returned values ride onto the model-call span and its children -- that is the
78
+ reason this surface exists. The returned field is named `runtimeContext`, not `metadata`, because AI
79
+ SDK v7 carries per-call attributes on runtime context rather than a dedicated metadata field.
105
80
 
106
- ```ts
107
- // agent/channels/support.ts
108
- import { defineChannel, POST } from "experimental-ash/channels";
109
-
110
- export default defineChannel({
111
- state: { ticketId: null as string | null },
112
- // Ash uses this function's runtime value for instrumentation metadata and
113
- // makes its return type available to instrumentation.ts.
114
- metadata(state) {
115
- return { ticketId: state.ticketId };
116
- },
117
- routes: [POST("/support", async () => new Response("ok"))],
118
- });
119
- ```
81
+ Use it when the values depend on the current session, turn, step, channel, or model input:
120
82
 
121
83
  ```ts
122
- // agent/instrumentation.ts
123
84
  import { defineInstrumentation, isChannel } from "experimental-ash/instrumentation";
124
-
125
85
  import supportChannel from "./channels/support.js";
126
86
 
127
87
  export default defineInstrumentation({
128
- metadata: {
88
+ events: {
129
89
  "step.started"(input) {
130
90
  if (!isChannel(input.channel, supportChannel)) {
131
- return {};
91
+ return undefined;
132
92
  }
133
93
 
134
94
  return {
135
- "support.ticket_id": input.channel.metadata.ticketId ?? "",
95
+ runtimeContext: {
96
+ "support.channel_id": input.channel.metadata.channelId ?? "",
97
+ "support.user_id": input.channel.metadata.triggeringUserId ?? "",
98
+ },
136
99
  };
137
100
  },
138
101
  },
139
102
  });
140
103
  ```
141
104
 
142
- Built-in wrapper return types expose their concrete metadata too. For example, a file named
143
- `agent/channels/support.ts` that default-exports `slackChannel()` still narrows under
144
- `input.channel.kind === "channel:support"` to Slack's projected metadata shape.
145
-
146
- The callback runs after Ash has assembled the model input for the attempt and before constructing
147
- the AI SDK call. That timing lets the returned values attach to the AI SDK model-call span and its
148
- child spans. It runs for each model-call attempt, including a retry of the same logical step when
149
- Ash changes provider settings or instructions.
105
+ For authored channels, Ash emits compiler-owned typings keyed by the channel filename. A file at
106
+ `agent/channels/support.ts` narrows as `channel:support`, either by checking
107
+ `input.channel.kind === "channel:support"` or by using `isChannel(input.channel, supportChannel)`.
150
108
 
151
109
  The callback receives:
152
110
 
@@ -160,7 +118,8 @@ The callback receives:
160
118
  A channel exposes its identity through `kind`: the discriminant you narrow on. For authored
161
119
  channels it is `channel:<name>`, where `<name>` is the channel's filename under
162
120
  `agent/channels/` — `agent/channels/support.ts` is `channel:support`. Framework channels use
163
- `http`, `schedule`, or `subagent`. It is also emitted as the `ash.channel.kind` span attribute.
121
+ `http`, `schedule`, or `subagent`; an unrecognized or absent kind normalizes to `unknown`. The kind
122
+ is also emitted as the `ash.channel.kind` span attribute.
164
123
 
165
124
  Channel metadata is channel-owned. Built-in channels expose only the fields they choose to make
166
125
  observable; for example, Slack projects `channelId`, `teamId`, `threadTs`, and `triggeringUserId`
@@ -168,36 +127,12 @@ from its durable channel state. User-authored channels expose their own projecti
168
127
  `metadata(state)` from `defineChannel`. Runtime instrumentation never falls back to raw channel
169
128
  state.
170
129
 
171
- `metadata(state)` must return an object composed of JSON primitives, arrays, and plain objects.
172
- Ash omits `undefined` object properties and drops projections containing values such as `Date` or
173
- `Map` with a warning.
174
-
175
- Manual `ChannelMetadataMap` declaration merging is only the escape hatch for unusual setups where
176
- the generated `.ash/**/*.d.ts` file is not available to the TypeScript program, or where a channel
177
- is typed outside the normal `agent/channels/<name>` compile path:
178
-
179
- ```ts
180
- import type { SlackInstrumentationMetadata } from "experimental-ash/channels/slack";
181
-
182
- declare module "experimental-ash/instrumentation" {
183
- interface ChannelMetadataMap {
184
- readonly "channel:support": SlackInstrumentationMetadata;
185
- }
186
- }
187
- ```
188
-
189
- Metadata failures are non-destructive. If the callback throws, returns a non-record, or returns
190
- non-string values, Ash logs a warning, drops the invalid metadata, and continues the model call.
191
- Thenables are rejected; the callback is intentionally synchronous. Keys beginning with `ash.` are
192
- reserved, so authored metadata cannot override framework metadata. Instrumentation projections
193
- containing values outside the JSON shape, such as `Date` or `Map`, are dropped with a warning.
194
-
195
130
  ## Trace Hierarchy
196
131
 
197
132
  When telemetry is enabled, each turn produces a trace like:
198
133
 
199
134
  ```text
200
- ash.turn {ash.session.id, ash.turn.id}
135
+ ai.ash.turn {ash.session.id}
201
136
  +-- ai.streamText step 1
202
137
  | +-- ai.streamText.doStream model call
203
138
  | +-- ai.toolCall {toolName: search} tool exec
@@ -207,10 +142,43 @@ ash.turn {ash.session.id, ash.turn.id}
207
142
  +-- ai.streamText step 3 (final text)
208
143
  ```
209
144
 
210
- Ash creates the `ash.turn` parent span per turn and passes enriched telemetry to the AI SDK so model
211
- calls and tool executions are traced automatically. Session, turn, step, and channel context
212
- (`ash.version`, `ash.session.id`, `ash.environment`, `ash.turn.id`, `ash.turn.sequence`,
213
- `ash.step.index`, `ash.channel.kind`) is injected into span metadata.
145
+ Ash creates the `ai.ash.turn` parent span per turn and passes enriched telemetry to the AI SDK so model
146
+ calls and tool executions are traced automatically. Session, turn, step, and channel context is
147
+ injected as the framework half of the runtime context (`ash.version`, `ash.session.id`,
148
+ `ash.environment`, `ash.turn.id`, `ash.turn.sequence`, `ash.step.index`, `ash.channel.kind`) and
149
+ rides onto the spans alongside any values your `events["step.started"]` callback returns under
150
+ `runtimeContext`.
151
+
152
+ ## Workflow run tags
153
+
154
+ Separately from OpenTelemetry, Ash tags every workflow run with reserved `$ash.*` attributes. These
155
+ live on the **Vercel Workflow run** -- queryable in the Workflow dashboard -- not on OTel spans, and
156
+ you do not configure them: they are framework-owned and emitted automatically on every session,
157
+ turn, and subagent run, whether or not an `instrumentation.ts` file is present. Authored code cannot
158
+ set or override the `$ash.` namespace.
159
+
160
+ Their job is to let a dashboard reconstruct the tree of runs behind a single agent invocation and
161
+ surface model and token usage without reading run bodies.
162
+
163
+ **Structural tags** describe each run's place in the tree:
164
+
165
+ - `$ash.type` -- `"session"`, `"turn"`, or `"subagent"`
166
+ - `$ash.parent` -- session id of the immediate parent
167
+ - `$ash.root` -- session id of the root session in the chain (group a whole tree with
168
+ `$ash.root=<id>`)
169
+ - `$ash.subagent` -- compiled graph node id (subagent runs only)
170
+ - `$ash.trigger` -- the channel kind that started the run
171
+ - `$ash.title` -- truncated title derived from the first user message
172
+
173
+ **Per-turn usage tags** are written on each step of a turn, accumulating cumulative totals
174
+ (last write wins):
175
+
176
+ - `$ash.model` -- model id for the turn
177
+ - `$ash.input_tokens`, `$ash.output_tokens`, `$ash.cache_read_tokens` -- running token counts
178
+ - `$ash.tool_count` -- number of tools available to the turn
179
+
180
+ Tag writes are best-effort: a failure is logged once per process and then swallowed, so a broken tag
181
+ emit never breaks the agent.
214
182
 
215
183
  ## What To Read Next
216
184
 
@@ -113,6 +113,7 @@ Channel and Slack types exported from `experimental-ash/channels/slack`:
113
113
  - `SlackInteractionAction` - action type for `onInteraction`
114
114
  - `SlackMentionResult` / `SlackInboundResult` - return type of `onAppMention` / `onDirectMessage`
115
115
  (`{ auth, context? } | null`)
116
+ - `SlackReceiveTarget` - target accepted by proactive `receive(slack, ...)`
116
117
  - `defaultSlackAuth(message, ctx)` - default Slack actor-to-session-auth projection
117
118
  - `Card`, `Button`, `Actions`, `Section`, `Modal`, `Table`, etc. - card builders re-exported for
118
119
  rendering Slack messages
@@ -134,6 +135,7 @@ Channel and Twilio types exported from `experimental-ash/channels/twilio`:
134
135
  - `TwilioVoiceResult` - call-answering options returned by `onVoice`
135
136
  - `TwilioVoiceTranscription` - parsed voice transcript payload (`from`, `to`, `callSid`, `text`,
136
137
  `confidence`, ...)
138
+ - `TwilioReceiveTarget` - target accepted by proactive `receive(twilio, ...)`
137
139
  - `verifyTwilioRequest`, `signTwilioRequest` - Ash-owned Twilio webhook signature helpers
138
140
 
139
141
  Channel and Telegram types exported from `experimental-ash/channels/telegram`:
@@ -152,7 +154,7 @@ Channel and Telegram types exported from `experimental-ash/channels/telegram`:
152
154
  `attachments`, reply context, ...)
153
155
  - `TelegramCallbackQuery` - parsed callback query payload passed to `onCallbackQuery`
154
156
  - `TelegramAttachment` - parsed inbound photo or document metadata
155
- - `TelegramReceiveArgs` - arguments accepted by proactive `receive(telegram, ...)`
157
+ - `TelegramReceiveTarget` - target accepted by proactive `receive(telegram, ...)`
156
158
  - `verifyTelegramRequest` - Ash-owned webhook secret-token verification helper
157
159
 
158
160
  Channel and Microsoft Teams types exported from `experimental-ash/channels/teams`:
@@ -166,7 +168,7 @@ Channel and Microsoft Teams types exported from `experimental-ash/channels/teams
166
168
  `updateActivity`, `startTyping`, and `request`
167
169
  - `TeamsThread` - conversation-scoped `post`, `update`, `startTyping`, and `mentionUser`
168
170
  - `TeamsMessageActivity` / `TeamsInvokeActivity` - parsed inbound Activity payloads
169
- - `TeamsReceiveArgs` - proactive/cross-channel handoff args requiring `serviceUrl` and
171
+ - `TeamsReceiveTarget` - proactive/cross-channel handoff target requiring `serviceUrl` and
170
172
  `conversationId`
171
173
  - `teamsContinuationToken` - channel-local Teams continuation-token helper
172
174
  - `defaultTeamsAuth` - default Teams actor-to-session-auth projection
@@ -143,7 +143,7 @@ Component and modal submissions resume the parked Ash session automatically.
143
143
 
144
144
  ## Proactive Sessions
145
145
 
146
- Use `receive(discord, { message, args, auth })` from a schedule `run` handler, or
146
+ Use `receive(discord, { message, target, auth })` from a schedule `run` handler, or
147
147
  `args.receive(discord, ...)` from another channel, to start a Discord session:
148
148
 
149
149
  ```ts
@@ -156,7 +156,7 @@ export default defineSchedule({
156
156
  waitUntil(
157
157
  receive(discord, {
158
158
  message: "Post the daily summary.",
159
- args: {
159
+ target: {
160
160
  channelId: "123456789012345678",
161
161
  initialMessage: "Daily summary",
162
162
  },
@@ -373,7 +373,7 @@ Adaptive Cards, and sends agent replies through the Bot Framework Connector REST
373
373
  resume by conversation id; channel and group-chat threads resume by conversation id plus root
374
374
  activity id.
375
375
 
376
- Proactive `receive(teams, args)` sessions require an existing conversation reference
376
+ Proactive `receive(teams, { target })` sessions require an existing conversation reference
377
377
  (`serviceUrl` and `conversationId`). See [Microsoft Teams channel setup](./teams.mdx) for Azure Bot
378
378
  setup, environment variables, proactive handoff, HITL, and file options.
379
379
 
@@ -394,7 +394,7 @@ export default defineChannel({
394
394
  args.waitUntil(
395
395
  args.receive(slack, {
396
396
  message: `Investigate ${incident.reference}: ${incident.title}`,
397
- args: { channelId: "C0123ABC" },
397
+ target: { channelId: "C0123ABC" },
398
398
  auth: {
399
399
  authenticator: "incidentio",
400
400
  principalType: "service",
@@ -412,7 +412,7 @@ export default defineChannel({
412
412
  Semantics:
413
413
 
414
414
  - The target channel's authored `receive(input, { send })` hook owns the continuation-token
415
- format and the initial state. Callers supply only `{ message, args, auth }`.
415
+ format and the initial state. Callers supply only `{ message, target, auth }`.
416
416
  - `auth` flows through to `session.auth.initiator` so the target's event handlers and the
417
417
  agent's tools can read who started the session.
418
418
  - Calling `args.receive(...)` does **not** also start a session on the current channel. The
@@ -441,7 +441,7 @@ import { Card, CardText } from "experimental-ash/channels/slack";
441
441
 
442
442
  await args.receive(slack, {
443
443
  message: "Begin investigation",
444
- args: {
444
+ target: {
445
445
  channelId: "C0123ABC",
446
446
  initialMessage: {
447
447
  card: Card({ children: [CardText("Investigation Thread for INC-42")] }),
@@ -84,7 +84,7 @@ invoke activities can be handled with `onInvoke(ctx, activity)`.
84
84
 
85
85
  ## Proactive Sessions
86
86
 
87
- Use `receive(teams, args)` only when you already have a Teams conversation reference. Teams v1 does
87
+ Use `receive(teams, { target })` only when you already have a Teams conversation reference. Teams v1 does
88
88
  not create new chats by AAD user id.
89
89
 
90
90
  ```ts
@@ -174,7 +174,7 @@ channel posts, stickers, and outbound sandbox-file sharing are not included.
174
174
 
175
175
  ## Proactive Sessions
176
176
 
177
- Use `receive(telegram, { message, args, auth })` from a schedule `run` handler, or
177
+ Use `receive(telegram, { message, target, auth })` from a schedule `run` handler, or
178
178
  `args.receive(telegram, ...)` from another channel, to start a Telegram session:
179
179
 
180
180
  ```ts
@@ -187,7 +187,7 @@ export default defineSchedule({
187
187
  waitUntil(
188
188
  receive(telegram, {
189
189
  message: "Post the daily summary.",
190
- args: {
190
+ target: {
191
191
  chatId: "123456789",
192
192
  initialMessage: "Daily summary",
193
193
  },
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "pages": [
3
3
  "getting-started",
4
+ "onboarding",
4
5
  "---",
5
6
  "agent-ts",
6
7
  "skills",
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: "Onboarding"
3
+ description: "An executable procedure for an AI agent to set up a new Ash agent across channels on the user's behalf."
4
+ ---
5
+
6
+ # Agent Onboarding Skill
7
+
8
+ This page is an **executable procedure for an AI coding agent** (for example, Claude Code) to create and wire up a new Ash agent across channels on the user's behalf, end to end. A human runs the agent; the agent runs this skill.
9
+
10
+ The flow is designed to be **seamless**: the agent asks the user only for genuine decisions and for the few browser/OAuth steps that cannot be automated, and verifies every step against machine-readable output before reporting success.
11
+
12
+ ## When to use
13
+
14
+ Use this when the user wants to create a new Ash agent — "set up an Ash agent", "scaffold an agent", "create an agent on Slack and web". If an Ash app already exists in the working directory, skip scaffolding and jump to [Adding a channel later](#adding-a-channel-later).
15
+
16
+ ## Prerequisites
17
+
18
+ - Node `>=24` and `pnpm`.
19
+ - For Slack or a Vercel deployment: the `vercel` CLI, logged in. Check with `vercel whoami`. If it is not installed, install it with `pnpm add -g vercel`; to log in, hand the user `vercel login` (it opens a browser).
20
+
21
+ ## Decisions to collect
22
+
23
+ Collect these up front. A capable agent should use a structured question UI (such as AskUserQuestion) so the user picks rather than free-types:
24
+
25
+ 1. **Name** — the project / directory name (kebab-case). Also used as the Slack connector slug.
26
+ 2. **Model** — for example `anthropic/claude-sonnet-4.6` or `openai/gpt-5-mini`. Baked into `agent/agent.ts`.
27
+ 3. **Channels** — `web` can be scaffolded headlessly. `slack` is an interactive follow-up because Vercel Connect opens a browser OAuth flow. The local REPL is always available via `ash dev`, regardless of channel choice.
28
+ 4. **Model provider** — how the agent reaches a model:
29
+ - Vercel project (default) — create a project, or pass `--project <slug>` to link an existing one, and use AI Gateway via OIDC.
30
+ - API key override — pass `--gateway-api-key <key>` to write `AI_GATEWAY_API_KEY` to `.env.local`.
31
+ - Local only — pass `--local-only` for web/REPL-only setups that should scaffold without a Vercel project; the agent will not reach a model until you add a provider. Slack still requires a Vercel project.
32
+ 5. **Deploy** — whether to deploy to Vercel production now. Required for Slack to receive events; pass `--no-deploy` to skip.
33
+
34
+ ## Step 1 — Scaffold (non-interactive)
35
+
36
+ Run the create CLI in headless mode. It scaffolds the project, provisions the model provider, and emits a structured JSON event stream. If the user chose Slack, do **not** pass `slack` here; add it in [Step 2](#step-2--add-slack-interactively).
37
+
38
+ ```bash
39
+ npx create-experimental-ash-agent@latest <name> \
40
+ --model <model> \
41
+ --channels web \
42
+ [--team <slug>] \
43
+ [--project <slug>] \
44
+ [--gateway-api-key <key>] \
45
+ [--local-only] \
46
+ [--no-deploy] \
47
+ --target-dir <parent-dir> \
48
+ --yes --json
49
+ ```
50
+
51
+ By default the CLI creates a Vercel project named `<name>`, links it non-interactively, and pulls the project's AI Gateway environment. Use `--project <slug>` when the user picked an existing project, `--team <slug>` when they picked a non-current Vercel team, `--gateway-api-key <key>` when they want a pasted key in `.env.local`, and `--local-only` only for web/REPL-only setups where they explicitly do not want Vercel provisioning.
52
+
53
+ The CLI advances as far as it can without human input. When it reaches a login or browser step, it emits an `action-required` record and exits cleanly instead of blocking on a prompt:
54
+
55
+ ```json
56
+ {
57
+ "type": "action-required",
58
+ "kind": "vercel-login",
59
+ "command": "vercel login",
60
+ "reason": "Provisioning a Vercel project requires you to be logged in to Vercel."
61
+ }
62
+ ```
63
+
64
+ When you see `action-required`: present `command` to the user to run themselves, wait for them to confirm, then **re-run the exact same create command**. Repeat until the stream emits `{ "type": "done" }`.
65
+
66
+ > If the installed CLI does not support `--json` / headless flags (an older version), fall back to the interactive wizard `pnpm create experimental-ash-agent` and walk the user through the same decisions above.
67
+
68
+ ## Step 2 — Add Slack interactively
69
+
70
+ If the user chose Slack, run the supported interactive channel setup from the project directory:
71
+
72
+ ```bash
73
+ cd <project-dir>
74
+ ash channels add slack
75
+ ```
76
+
77
+ That Ash command creates the Slack connector through Vercel Connect, attaches it to the Ash Slack route (`/ash/v1/slack`), and deploys the project. The underlying Vercel Connect command it runs is:
78
+
79
+ ```bash
80
+ vercel connect create slack --triggers --name <name>
81
+ ```
82
+
83
+ Do not try to drive Slack through headless `create-experimental-ash-agent --channels slack`. Slack setup needs an interactive browser OAuth flow and is intentionally separate from headless scaffolding.
84
+
85
+ ## Step 3 — Verify
86
+
87
+ From the project directory:
88
+
89
+ ```bash
90
+ ash build
91
+ ash info --json # compiled routes, channels, tools, model, status
92
+ ash channels list --json # e.g. { "channels": ["slack", "web"] }
93
+ ```
94
+
95
+ For Slack, confirm the connector is attached to this project:
96
+
97
+ ```bash
98
+ vercel connect list -F json --all-projects
99
+ ```
100
+
101
+ Treat setup as successful only when `ash info --json` reports `status: "ready"` and the channels array contains every channel the user selected.
102
+
103
+ ## Step 4 — Run and test locally
104
+
105
+ - Web chat: `pnpm dev`, open `http://localhost:3000`, send a message.
106
+ - REPL: `ash dev`, chat in the terminal.
107
+
108
+ The agent replies only if it can reach a model: either `AI_GATEWAY_API_KEY` is set in `.env.local`, or the Vercel project is linked and you have run `vercel env pull --yes`.
109
+
110
+ ## Adding a channel later
111
+
112
+ In an existing Ash project, add a channel without re-scaffolding:
113
+
114
+ ```bash
115
+ ash channels add web --yes
116
+ ash channels add slack
117
+ ```
118
+
119
+ This runs the same channel setup — and, for Slack, the same connector and deploy steps — as the create flow.
@@ -41,7 +41,7 @@ export default defineSchedule({
41
41
  waitUntil(
42
42
  receive(slack, {
43
43
  message: "Summarize yesterday's activity and post the digest.",
44
- args: { channelId: "C0123ABC" },
44
+ target: { channelId: "C0123ABC" },
45
45
  auth: appAuth,
46
46
  }),
47
47
  );
@@ -51,7 +51,7 @@ export default defineSchedule({
51
51
 
52
52
  `ScheduleHandlerArgs`:
53
53
 
54
- - `receive(channel, { message, args, auth })` — same contract as a route handler's [`args.receive`](./channels/README.md#cross-channel-hand-off). Hands the work off to a channel's authored `receive` hook.
54
+ - `receive(channel, { message, target, auth })` — same contract as a route handler's [`args.receive`](./channels/README.md#cross-channel-hand-off). Hands the work off to a channel's authored `receive` hook.
55
55
  - `waitUntil(promise)` — extends the cron task's lifetime past handler return so in-flight work settles before the Nitro task completes.
56
56
  - `appAuth` — pre-built APP auth context (`{ authenticator: "app", principalId: "ash:app", principalType: "runtime" }`). Pass to `receive(..., { auth: appAuth })` for schedules that run on behalf of the agent itself.
57
57
 
@@ -81,7 +81,7 @@ export default defineSchedule({
81
81
  waitUntil(
82
82
  receive(slack, {
83
83
  message: "Summarize today's production deploys.",
84
- args: {
84
+ target: {
85
85
  channelId: "C0123ABC",
86
86
  initialMessage: {
87
87
  card: Card({ children: [CardText("Daily Deploy Digest")] }),
@@ -95,7 +95,7 @@ export default defineSchedule({
95
95
  });
96
96
  ```
97
97
 
98
- `threadTs` and `initialMessage` are mutually exclusive on Slack receive args.
98
+ `threadTs` and `initialMessage` are mutually exclusive on Slack receive targets.
99
99
 
100
100
  ### Markdown (`markdown`)
101
101
 
@@ -3,7 +3,7 @@ import type { RouteHandler, SendFn } from "#channel/routes.js";
3
3
  import type { Session } from "#channel/session.js";
4
4
  import type { SessionAuthContext } from "#channel/types.js";
5
5
  export declare const CHANNEL_SENTINEL: "ash:channel";
6
- export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<string, unknown>, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
6
+ export interface CompiledChannel<TState = undefined, TReceiveTarget = Record<string, unknown>, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
7
7
  readonly __kind: typeof CHANNEL_SENTINEL;
8
8
  readonly routes: readonly {
9
9
  method: string;
@@ -14,7 +14,7 @@ export interface CompiledChannel<TState = undefined, TReceiveArgs = Record<strin
14
14
  readonly __metadata?: TMetadata;
15
15
  readonly receive?: (input: {
16
16
  readonly message: string;
17
- readonly args: Readonly<TReceiveArgs>;
17
+ readonly target: Readonly<TReceiveTarget>;
18
18
  readonly auth: SessionAuthContext | null;
19
19
  }, args: {
20
20
  send: SendFn<TState>;
@@ -1,7 +1,7 @@
1
1
  import type { UserContent } from "ai";
2
2
  import type { ChannelAdapter } from "#channel/adapter.js";
3
3
  import { type CompiledChannel } from "#channel/compiled-channel.js";
4
- import type { InferReceiveArgs } from "#channel/receive-args.js";
4
+ import type { InferReceiveTarget } from "#channel/receive-target.js";
5
5
  import type { Session } from "#channel/session.js";
6
6
  import type { Runtime, SessionAuthContext } from "#channel/types.js";
7
7
  import type { ResolvedChannelDefinition } from "#runtime/types.js";
@@ -9,11 +9,11 @@ import type { ResolvedChannelDefinition } from "#runtime/types.js";
9
9
  * Options accepted by {@link CrossChannelReceiveFn}. Mirrors the input
10
10
  * argument of a channel's authored `receive(input, { send })` hook —
11
11
  * the runtime constructs `send` internally so route-handler callers
12
- * only supply the platform args, payload, and auth.
12
+ * only supply the platform target, payload, and auth.
13
13
  */
14
- export interface CrossChannelReceiveOptions<TArgs = Record<string, unknown>> {
14
+ export interface CrossChannelReceiveOptions<TTarget = Record<string, unknown>> {
15
15
  readonly message: string | UserContent;
16
- readonly args: TArgs;
16
+ readonly target: TTarget;
17
17
  readonly auth: SessionAuthContext | null;
18
18
  }
19
19
  /**
@@ -22,7 +22,7 @@ export interface CrossChannelReceiveOptions<TArgs = Record<string, unknown>> {
22
22
  * format and initial state; `auth` is forwarded verbatim and becomes
23
23
  * `session.initiatorAuth`.
24
24
  */
25
- export type CrossChannelReceiveFn = <TChannel>(channel: TChannel, options: CrossChannelReceiveOptions<InferReceiveArgs<TChannel>>) => Promise<Session>;
25
+ export type CrossChannelReceiveFn = <TChannel>(channel: TChannel, options: CrossChannelReceiveOptions<InferReceiveTarget<TChannel>>) => Promise<Session>;
26
26
  /**
27
27
  * Channel record consumed by the receiver — keeps the public-facing
28
28
  * `definition` reference so callers can identify a target by value
@@ -54,7 +54,7 @@ interface InvokeChannelReceiveInput {
54
54
  readonly target: Pick<CrossChannelTarget, "name" | "receive" | "adapter">;
55
55
  readonly input: {
56
56
  readonly message: string;
57
- readonly args: Readonly<Record<string, unknown>>;
57
+ readonly target: Readonly<Record<string, unknown>>;
58
58
  readonly auth: SessionAuthContext | null;
59
59
  };
60
60
  readonly describeMissingReceive: () => string;