experimental-ash 0.51.0 → 0.53.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 +14 -0
- package/dist/docs/public/advanced/instrumentation.md +87 -119
- package/dist/docs/public/advanced/session-context.md +4 -2
- package/dist/docs/public/advanced/typescript-api.md +4 -2
- package/dist/docs/public/channels/discord.mdx +2 -2
- package/dist/docs/public/channels/index.md +4 -4
- package/dist/docs/public/channels/teams.mdx +1 -1
- package/dist/docs/public/channels/telegram.mdx +2 -2
- package/dist/docs/public/meta.json +1 -0
- package/dist/docs/public/onboarding.md +119 -0
- package/dist/docs/public/schedules.mdx +4 -4
- package/dist/docs/public/subagents.mdx +1 -1
- package/dist/src/channel/compiled-channel.d.ts +2 -2
- package/dist/src/channel/cross-channel-receive.d.ts +6 -6
- package/dist/src/channel/cross-channel-receive.js +1 -1
- package/dist/src/channel/receive-target.d.ts +17 -0
- package/dist/src/channel/types.d.ts +4 -0
- package/dist/src/cli/commands/channels.d.ts +2 -0
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/cli/commands/info.d.ts +46 -1
- package/dist/src/cli/commands/info.js +2 -2
- package/dist/src/cli/run.d.ts +3 -1
- package/dist/src/cli/run.js +2 -2
- package/dist/src/execution/ash-workflow-attributes.d.ts +18 -0
- package/dist/src/execution/ash-workflow-attributes.js +1 -1
- package/dist/src/execution/create-session-step.js +1 -1
- package/dist/src/execution/subagent-tool.js +1 -1
- package/dist/src/harness/{instrumentation-metadata.d.ts → instrumentation-runtime-context.d.ts} +2 -2
- package/dist/src/harness/instrumentation-runtime-context.js +1 -0
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/instrumentation.d.ts +8 -7
- package/dist/src/internal/instrumentation.js +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +2 -2
- package/dist/src/packages/ash-scaffold/src/human-action.js +1 -0
- package/dist/src/packages/ash-scaffold/src/index.js +1 -1
- package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +1 -1
- package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -1
- package/dist/src/public/channels/discord/discordChannel.d.ts +3 -3
- package/dist/src/public/channels/discord/discordChannel.js +1 -1
- package/dist/src/public/channels/discord/index.d.ts +1 -1
- package/dist/src/public/channels/slack/index.d.ts +1 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +4 -3
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/public/channels/teams/index.d.ts +1 -1
- package/dist/src/public/channels/teams/teamsChannel.d.ts +3 -3
- package/dist/src/public/channels/teams/teamsChannel.js +1 -1
- package/dist/src/public/channels/telegram/index.d.ts +1 -1
- package/dist/src/public/channels/telegram/telegramChannel.d.ts +3 -3
- package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
- package/dist/src/public/channels/twilio/index.d.ts +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.d.ts +3 -3
- package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
- package/dist/src/public/definitions/defineChannel.d.ts +8 -8
- package/dist/src/public/definitions/schedule.d.ts +2 -2
- package/dist/src/public/instrumentation/index.d.ts +21 -11
- package/dist/src/public/schedules/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/src/channel/receive-args.d.ts +0 -17
- package/dist/src/harness/instrumentation-metadata.js +0 -1
- /package/dist/src/channel/{receive-args.js → receive-target.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.53.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3ea7d16: Expose the parent subagent tool call id on `ctx.session.parent.callId` for local subagent sessions. Subagent workflow runs now use `$ash.parent` for the parent session id and add `$ash.parent_call` / `$ash.parent_turn` for lineage queries.
|
|
8
|
+
|
|
9
|
+
## 0.52.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
- b91b550: Breaking: replace `defineInstrumentation({ metadata })` with `defineInstrumentation({ events: { "step.started": () => ({ runtimeContext }) } })` for per-step AI SDK telemetry context.
|
|
16
|
+
|
|
3
17
|
## 0.51.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "instrumentation.ts"
|
|
3
|
-
description: "Configure OpenTelemetry
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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.
|
|
34
|
-
telemetry -- there is no separate `isEnabled` toggle.
|
|
47
|
+
Export the result of `defineInstrumentation` as the default export.
|
|
35
48
|
|
|
36
|
-
##
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
`agent/channels/support.ts`
|
|
144
|
-
`input.channel.kind === "channel:support"`
|
|
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
|
|
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
|
|
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`,
|
|
213
|
-
`ash.step.index`, `ash.channel.kind`)
|
|
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
|
|
|
@@ -35,7 +35,8 @@ export default defineTool({
|
|
|
35
35
|
turnSequence: ctx.session.turn.sequence,
|
|
36
36
|
currentCaller: ctx.session.auth.current?.principalId,
|
|
37
37
|
initiator: ctx.session.auth.initiator?.principalId,
|
|
38
|
-
parentSessionId: ctx.session.parent?.
|
|
38
|
+
parentSessionId: ctx.session.parent?.sessionId,
|
|
39
|
+
parentCallId: ctx.session.parent?.callId,
|
|
39
40
|
};
|
|
40
41
|
},
|
|
41
42
|
});
|
|
@@ -56,7 +57,8 @@ Important behavior:
|
|
|
56
57
|
- `auth.initiator` is the caller that started the durable session
|
|
57
58
|
- unprotected agents expose both as `null`
|
|
58
59
|
- top-level schedule sessions expose the framework app principal (`principalId: "ash:app"`, `principalType: "runtime"`)
|
|
59
|
-
- `parent` is present for child subagent sessions
|
|
60
|
+
- `parent` is present for child subagent sessions and includes the parent `callId`, `sessionId`,
|
|
61
|
+
`rootSessionId`, and `turn`
|
|
60
62
|
|
|
61
63
|
## `ctx.getSandbox()`
|
|
62
64
|
|
|
@@ -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
|
-
- `
|
|
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
|
-
- `
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
190
|
+
target: {
|
|
191
191
|
chatId: "123456789",
|
|
192
192
|
initialMessage: "Daily summary",
|
|
193
193
|
},
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
98
|
+
`threadTs` and `initialMessage` are mutually exclusive on Slack receive targets.
|
|
99
99
|
|
|
100
100
|
### Markdown (`markdown`)
|
|
101
101
|
|
|
@@ -118,7 +118,7 @@ Subagent execution gets:
|
|
|
118
118
|
- its own tools
|
|
119
119
|
- its own sandbox (independent of the parent's sandbox)
|
|
120
120
|
- its own `skills/` set (independent of the parent's skills)
|
|
121
|
-
- immediate parent lineage in `ctx.session.parent`
|
|
121
|
+
- immediate parent lineage in `ctx.session.parent`, including the parent subagent tool `callId`
|
|
122
122
|
|
|
123
123
|
Skills and sandboxes do not cross the parent/child boundary. The subagent only sees skills
|
|
124
124
|
authored under `agent/subagents/<id>/skills/`; skills under the root `agent/skills/` are not
|
|
@@ -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,
|
|
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
|
|
17
|
+
readonly target: Readonly<TReceiveTarget>;
|
|
18
18
|
readonly auth: SessionAuthContext | null;
|
|
19
19
|
}, args: {
|
|
20
20
|
send: SendFn<TState>;
|