experimental-ash 0.18.3 → 0.20.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 +35 -0
- package/dist/docs/internals/context.md +7 -0
- package/dist/docs/public/channels/README.md +5 -0
- package/dist/docs/public/channels/slack.md +58 -4
- package/dist/docs/public/hooks.md +4 -2
- package/dist/docs/public/sandbox.md +71 -49
- package/dist/docs/public/tools.md +55 -3
- package/dist/docs/public/typescript-api.md +6 -1
- package/dist/src/channel/adapter.js +12 -2
- package/dist/src/channel/routes.d.ts +9 -1
- package/dist/src/channel/send.js +3 -3
- package/dist/src/channel/types.d.ts +3 -1
- package/dist/src/chunks/{dev-authored-source-watcher-j7YWh2Gx.js → dev-authored-source-watcher-NA8BZIXr.js} +1 -1
- package/dist/src/chunks/host-DOH_6p0f.js +22 -0
- package/dist/src/chunks/{paths-Dwv0Eash.js → paths-CbL9m08U.js} +25 -25
- package/dist/src/chunks/{prewarm-CQYfka30.js → prewarm-Bz6Jr9J1.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +7 -7
- package/dist/src/compiled/@ai-sdk/anthropic/index.js +2 -2
- package/dist/src/compiled/@ai-sdk/anthropic/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/google/index.js +6 -6
- package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/index.js +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/openai/index.js +6 -10
- package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/otel/index.js +2 -2
- package/dist/src/compiled/@ai-sdk/otel/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/provider/package.json +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +6 -1
- package/dist/src/compiled/@workflow/core/index.js +1 -1
- package/dist/src/compiled/@workflow/core/runtime.js +5 -5
- package/dist/src/compiled/@workflow/core/workflow.js +1 -1
- package/dist/src/compiled/@workflow/errors/index.js +1 -1
- package/dist/src/compiled/_chunks/workflow/{context-errors-CmtmBosi.js → context-errors-zbKocOyk.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/dist-CpUQh3NH.js +14 -0
- package/dist/src/compiled/_chunks/workflow/{resume-hook-BqY8TqOE.js → resume-hook-CL8Ed91K.js} +2 -2
- package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +1 -0
- package/dist/src/context/hook-lifecycle.js +5 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/node-step.js +1 -0
- package/dist/src/execution/sandbox/bash-tool.js +1 -1
- package/dist/src/execution/sandbox/bindings/local.d.ts +14 -1
- package/dist/src/execution/sandbox/bindings/local.js +22 -16
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +6 -0
- package/dist/src/execution/sandbox/bindings/vercel.js +25 -14
- package/dist/src/execution/sandbox/glob-tool.js +1 -1
- package/dist/src/execution/sandbox/grep-tool.js +1 -1
- package/dist/src/execution/sandbox/lazy-backend.d.ts +15 -0
- package/dist/src/execution/sandbox/lazy-backend.js +33 -0
- package/dist/src/execution/sandbox/read-file-tool.js +1 -1
- package/dist/src/execution/sandbox/ripgrep-probe.js +1 -1
- package/dist/src/execution/sandbox/session.js +49 -31
- package/dist/src/execution/sandbox/stream-utils.d.ts +2 -2
- package/dist/src/execution/sandbox/stream-utils.js +1 -1
- package/dist/src/execution/sandbox/write-file-tool.js +3 -3
- package/dist/src/execution/skills/types.d.ts +1 -1
- package/dist/src/execution/workflow-entry.d.ts +2 -4
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/harness/attachment-staging.js +10 -11
- package/dist/src/harness/execute-tool.d.ts +1 -0
- package/dist/src/harness/messages.js +15 -0
- package/dist/src/harness/tools.js +5 -0
- package/dist/src/harness/types.d.ts +6 -7
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/sandbox.d.ts +8 -2
- package/dist/src/internal/authored-definition/sandbox.js +10 -2
- package/dist/src/internal/authored-definition/schema-backed.js +12 -1
- package/dist/src/internal/nitro/host/configure-nitro-routes.js +1 -1
- package/dist/src/internal/workflow-bundle/builder-support.d.ts +1 -0
- package/dist/src/internal/workflow-bundle/builder-support.js +14 -0
- package/dist/src/internal/workflow-bundle/builder.js +6 -6
- package/dist/src/public/channels/slack/api-encoding.d.ts +1 -1
- package/dist/src/public/channels/slack/api-encoding.js +1 -7
- package/dist/src/public/channels/slack/api.d.ts +5 -4
- package/dist/src/public/channels/slack/api.js +6 -6
- package/dist/src/public/channels/slack/index.d.ts +3 -0
- package/dist/src/public/channels/slack/index.js +2 -0
- package/dist/src/public/channels/slack/slackChannel.d.ts +12 -4
- package/dist/src/public/channels/slack/slackChannel.js +4 -1
- package/dist/src/public/channels/slack/thread.d.ts +26 -0
- package/dist/src/public/channels/slack/thread.js +45 -0
- package/dist/src/public/definitions/sandbox.d.ts +1 -1
- package/dist/src/public/definitions/tool.d.ts +25 -0
- package/dist/src/public/sandbox/backends/default.d.ts +16 -1
- package/dist/src/public/sandbox/backends/default.js +7 -19
- package/dist/src/public/sandbox/backends/local.d.ts +7 -4
- package/dist/src/public/sandbox/backends/local.js +7 -5
- package/dist/src/public/sandbox/backends/vercel.d.ts +9 -3
- package/dist/src/public/sandbox/backends/vercel.js +9 -3
- package/dist/src/public/sandbox/index.d.ts +3 -2
- package/dist/src/public/sandbox/local-sandbox.d.ts +7 -0
- package/dist/src/public/sandbox/local-sandbox.js +1 -0
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +13 -1
- package/dist/src/public/tools/index.d.ts +1 -1
- package/dist/src/runtime/resolve-sandbox.js +5 -1
- package/dist/src/runtime/resolve-tool.js +3 -0
- package/dist/src/runtime/skills/sandbox-access.js +3 -3
- package/dist/src/runtime/types.d.ts +17 -2
- package/dist/src/shared/sandbox-definition.d.ts +16 -1
- package/dist/src/shared/sandbox-session.d.ts +53 -121
- package/package.json +9 -9
- package/dist/src/chunks/host-C19hLVqS.js +0 -22
- package/dist/src/compiled/_chunks/workflow/dist-4zn5tehu.js +0 -10
- package/dist/src/compiled/_chunks/workflow/dist-DTWUhyDN.js +0 -5
- package/dist/src/compiled/_chunks/workflow/sleep-D30F1GSr.js +0 -1
- /package/dist/src/compiled/_chunks/workflow/{dist-B6aByiku.js → dist-0iNBqPYp.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{dist-CVo7knbW.js → dist-D774SUM4.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{src-Bc9OYRaN.js → src-ClRYdO4-.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{symbols-DkV1V0kM.js → symbols-D-4tVV8x.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-Cq5QjRq8.js → token-CsNmv7KW.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-Duaoxfi5.js → token-j5Cl4rrs.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d94e73f: feat(ash): add `toModelOutput` on tool definitions
|
|
8
|
+
|
|
9
|
+
Tools can now declare a `toModelOutput` function that controls what the model sees as the tool result. The full `execute()` return remains available to channel event handlers and hooks via `action.result` events. Delegates to the AI SDK's native `toModelOutput` support.
|
|
10
|
+
|
|
11
|
+
- b78ef37: chore(ash): update AI SDK to latest version and align `SandboxSession` definition
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- a4fab4d: fix(slack): send Slack Web API calls form-encoded.
|
|
16
|
+
|
|
17
|
+
`callSlackApi` previously defaulted to `application/json`, which Slack
|
|
18
|
+
rejects on a subset of methods (notably `conversations.replies`, which
|
|
19
|
+
backs `SlackThread.refresh()` and the public `loadThreadContextMessages`
|
|
20
|
+
helper). Calls to those endpoints failed with `invalid_arguments`,
|
|
21
|
+
leaving `recentMessages` empty on every valid thread reply.
|
|
22
|
+
|
|
23
|
+
Slack documents `application/x-www-form-urlencoded` as universally
|
|
24
|
+
supported and Slack's own SDK uses form encoding exclusively.
|
|
25
|
+
`callSlackApi` now does the same: the body is always form-encoded, the
|
|
26
|
+
redundant `encoding: "form"` arguments on the file-upload callsites are
|
|
27
|
+
removed, and the `encoding` parameter is gone.
|
|
28
|
+
|
|
29
|
+
- c465274: fix(ash): fix broken Nitro rebuilds due to partial stream trigger
|
|
30
|
+
|
|
31
|
+
## 0.19.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- bede748: feat(ash): give agent author control over backend specific sandbox creation options
|
|
36
|
+
- 08e8e82: feat(slack): support thread model context
|
|
37
|
+
|
|
3
38
|
## 0.18.3
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
|
@@ -82,6 +82,13 @@ runs inside step (4)'s ALS scope, before the harness step. See
|
|
|
82
82
|
[Hooks](./hooks.md) for the full pipeline and the
|
|
83
83
|
`SessionPreparedKey` flag's failure semantics.
|
|
84
84
|
|
|
85
|
+
`StepInput.modelContext` has two producers. Channels can provide
|
|
86
|
+
ephemeral messages through `SendPayload.modelContext`, and lifecycle
|
|
87
|
+
hooks can provide them through `LifecycleHookResult.modelContext`.
|
|
88
|
+
The merge order is channel first, then `lifecycle.session`, then
|
|
89
|
+
`lifecycle.turn`. The merged messages are visible to the next model
|
|
90
|
+
call only and are never persisted to durable session history.
|
|
91
|
+
|
|
85
92
|
## Channel Context
|
|
86
93
|
|
|
87
94
|
Channels set custom durable context keys inside `onDeliver(ctx, payload)`. The method receives a `ContextAccessor` and returns `Promise<StepInput>`. This runs once per turn (both initial `run()` and each `deliver()`), after auth keys are seeded. Channel code stays workflow-agnostic because it only touches the accessor.
|
|
@@ -328,6 +328,11 @@ await args.receive(slack, {
|
|
|
328
328
|
});
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
For inbound mentions in an existing Slack thread, `onAppMention` and
|
|
332
|
+
`onDirectMessage` may return `modelContext` to inject fetched thread
|
|
333
|
+
history into the next model call without persisting it. See
|
|
334
|
+
[Slack thread context](/docs/channels/slack#thread-context).
|
|
335
|
+
|
|
331
336
|
`threadTs` and `initialMessage` are mutually exclusive: pass `threadTs` to join an existing
|
|
332
337
|
thread, or `initialMessage` to anchor a new one. With neither, the first agent post anchors
|
|
333
338
|
the thread automatically.
|
|
@@ -159,10 +159,12 @@ export default slackChannel({
|
|
|
159
159
|
- **`onAppMention(ctx, message)`** -- decides whether to dispatch a turn for an inbound
|
|
160
160
|
`app_mention`, with what `auth` context, and runs any pre-dispatch side effects (typing
|
|
161
161
|
indicators, logging, feature-flag lookups). Return `{ auth }` to dispatch or `null` to silently
|
|
162
|
-
drop the mention.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
drop the mention. Return `{ auth, modelContext }` to add ephemeral messages to the next model
|
|
163
|
+
call without writing them to durable session history. May be sync or async; the framework awaits
|
|
164
|
+
the result before dispatching. Thrown errors are caught, logged, and drop the mention -- wrap
|
|
165
|
+
best-effort side effects in `try/catch` if you want them to be non-fatal. The default
|
|
166
|
+
`onAppMention` derives a workspace-scoped auth from the Slack actor and posts a `"Thinking…"`
|
|
167
|
+
typing indicator.
|
|
166
168
|
- **`onDirectMessage(ctx, message)`** -- same contract as `onAppMention`, but for direct messages
|
|
167
169
|
(Slack `message` events with `channel_type: "im"`). The framework filters bot-authored messages
|
|
168
170
|
(`bot_id` set, including the bot's own replies) and message subtypes (edits, deletes, joins)
|
|
@@ -177,6 +179,58 @@ export default slackChannel({
|
|
|
177
179
|
(`turn.started`, `message.completed`, `session.failed`, etc.). They run inside the workflow context,
|
|
178
180
|
not on the inbound webhook side.
|
|
179
181
|
|
|
182
|
+
### Thread Context
|
|
183
|
+
|
|
184
|
+
When the bot is mentioned in an existing Slack thread, the triggering mention is delivered to the
|
|
185
|
+
agent by default, but prior thread replies are not injected automatically. Fetch thread history in
|
|
186
|
+
`onAppMention` or `onDirectMessage` and return `modelContext` when the agent should see that
|
|
187
|
+
background on the next model call.
|
|
188
|
+
|
|
189
|
+
Use `since: "last-agent-reply"` for repeated tags in the same thread. It returns only messages
|
|
190
|
+
after the agent's last Slack reply and before the current mention, so the injected context stays
|
|
191
|
+
small and can be modeled as a `role: "user"` message without changing the system prompt.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import {
|
|
195
|
+
defaultSlackAuth,
|
|
196
|
+
loadThreadContextMessages,
|
|
197
|
+
slackChannel,
|
|
198
|
+
type ModelMessage,
|
|
199
|
+
} from "experimental-ash/channels/slack";
|
|
200
|
+
|
|
201
|
+
export default slackChannel({
|
|
202
|
+
async onAppMention(ctx, message) {
|
|
203
|
+
const auth = defaultSlackAuth(message, ctx);
|
|
204
|
+
|
|
205
|
+
const priorMessages = await loadThreadContextMessages(ctx.thread, message, {
|
|
206
|
+
since: "last-agent-reply",
|
|
207
|
+
});
|
|
208
|
+
if (priorMessages.length === 0) {
|
|
209
|
+
return { auth };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const transcript = priorMessages
|
|
213
|
+
.map((entry) => `${entry.isMe ? "you" : (entry.user ?? "user")}: ${entry.markdown}`)
|
|
214
|
+
.join("\n");
|
|
215
|
+
|
|
216
|
+
const modelContext: ModelMessage[] = [
|
|
217
|
+
{
|
|
218
|
+
role: "user",
|
|
219
|
+
content:
|
|
220
|
+
"Recent Slack thread messages since your last reply, oldest first. " +
|
|
221
|
+
"Use them as background context for the current mention.\n\n" +
|
|
222
|
+
transcript,
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
return { auth, modelContext };
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`modelContext` is one-shot context: it is used for the model call that dispatches this inbound
|
|
232
|
+
message and is never persisted to `session.history`.
|
|
233
|
+
|
|
180
234
|
### Direct Messages
|
|
181
235
|
|
|
182
236
|
Add `onDirectMessage` alongside `onAppMention` to handle 1:1 DMs:
|
|
@@ -81,8 +81,10 @@ model turn. `lifecycle.turn` runs **once per fresh delivery** — tool-loop
|
|
|
81
81
|
continuations and HITL resumes do not re-fire it.
|
|
82
82
|
|
|
83
83
|
Both keys may return `{ modelContext }`. Contributions are concatenated
|
|
84
|
-
session-then-turn
|
|
85
|
-
|
|
84
|
+
session-then-turn before the harness's next model call, and are never
|
|
85
|
+
written to durable history. Channels may also contribute the same
|
|
86
|
+
`modelContext` shape through `SendPayload`; channel-provided messages
|
|
87
|
+
appear before lifecycle hook messages.
|
|
86
88
|
|
|
87
89
|
### Seed durable context
|
|
88
90
|
|
|
@@ -53,7 +53,10 @@ The public lifecycle surface is intentionally small:
|
|
|
53
53
|
- `bootstrap({ use })` — template-scoped setup (runs once when the template is built). Call `use()`
|
|
54
54
|
to get a `SandboxSession` for filesystem setup. Only filesystem state survives snapshotting.
|
|
55
55
|
- `onSession({ use })` — durable-session-scoped setup (runs once per session). Call `use(opts?)` to
|
|
56
|
-
get the backend
|
|
56
|
+
get a `SandboxSession`; `opts` are forwarded to the backend's update path (the Vercel SDK's
|
|
57
|
+
`sandbox.update(...)`) before the session is returned. `onSession`'s `use()` never creates a
|
|
58
|
+
sandbox — creation happens at prewarm time and at first-time session-create, both driven by the
|
|
59
|
+
backend factory's options.
|
|
57
60
|
- `sandbox.resolvePath(path)` — translate a logical `/workspace/...` path into the live filesystem
|
|
58
61
|
- `sandbox.runCommand(command)` — run a shell command inside the sandbox
|
|
59
62
|
|
|
@@ -200,9 +203,11 @@ template before the authored `bootstrap` runs, so your bootstrap can read them.
|
|
|
200
203
|
- configuring per-user credentials for CLI tools
|
|
201
204
|
- writing one-time markers
|
|
202
205
|
|
|
203
|
-
Call `use(opts?)` to get
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
Call `use(opts?)` to get a `SandboxSession`. When `opts` are supplied, the backend forwards them
|
|
207
|
+
to its update path (for `vercelBackend()`, this is `sandbox.update(opts)`) before the session is
|
|
208
|
+
returned. The accepted option shape is whatever the backend's update call accepts — `onSession`'s
|
|
209
|
+
`use()` never invokes `Sandbox.create`. Default `Sandbox.create` options come from the backend
|
|
210
|
+
factory; `onSession`'s `use()` overrides any overlapping fields post-create.
|
|
206
211
|
|
|
207
212
|
Unlike `bootstrap`, `onSession` runs inside the active Ash runtime context, so user-scoped setup can
|
|
208
213
|
call `getSession()` and derive the current principal without baking credentials into the reusable
|
|
@@ -231,9 +236,8 @@ export default defineSandbox({
|
|
|
231
236
|
|
|
232
237
|
Ash ships two built-in backends, exposed as factory functions from `experimental-ash/sandbox`:
|
|
233
238
|
|
|
234
|
-
- `vercelBackend()` — runs the sandbox on Vercel Sandbox via `@vercel/sandbox`.
|
|
235
|
-
|
|
236
|
-
- `localBackend()` — runs the sandbox locally via `just-bash`. Default for `pnpm ash dev`.
|
|
239
|
+
- `vercelBackend(opts?)` — runs the sandbox on Vercel Sandbox via `@vercel/sandbox`.
|
|
240
|
+
- `localBackend(opts?)` — runs the sandbox locally via `just-bash`. Default for `pnpm ash dev`.
|
|
237
241
|
|
|
238
242
|
Attach a backend via `defineSandbox({ backend })`:
|
|
239
243
|
|
|
@@ -242,7 +246,7 @@ Attach a backend via `defineSandbox({ backend })`:
|
|
|
242
246
|
import { defineSandbox, vercelBackend } from "experimental-ash/sandbox";
|
|
243
247
|
|
|
244
248
|
export default defineSandbox({
|
|
245
|
-
backend: vercelBackend(),
|
|
249
|
+
backend: vercelBackend({ runtime: "node24", resources: { vcpus: 2 } }),
|
|
246
250
|
async bootstrap({ use }) {
|
|
247
251
|
const sandbox = await use();
|
|
248
252
|
await sandbox.runCommand("git clone https://example.com/repo.git repo");
|
|
@@ -258,76 +262,91 @@ When `backend` is omitted, Ash substitutes `defaultBackend()` at runtime, which
|
|
|
258
262
|
everywhere else. `pnpm ash dev` on a developer laptop therefore boots the local workspace by
|
|
259
263
|
default; production builds on Vercel use the Vercel backend by default.
|
|
260
264
|
|
|
261
|
-
|
|
265
|
+
`defaultBackend()` also accepts a keyed options bag so each inner backend gets its own typed
|
|
266
|
+
create options without forcing you to pin to one backend up front:
|
|
262
267
|
|
|
263
268
|
```ts
|
|
264
269
|
import { defaultBackend, defineSandbox } from "experimental-ash/sandbox";
|
|
265
270
|
|
|
266
271
|
export default defineSandbox({
|
|
267
|
-
backend: defaultBackend(
|
|
272
|
+
backend: defaultBackend({
|
|
273
|
+
vercel: { networkPolicy: "deny-all", resources: { vcpus: 4 } },
|
|
274
|
+
local: {},
|
|
275
|
+
}),
|
|
268
276
|
});
|
|
269
277
|
```
|
|
270
278
|
|
|
271
|
-
###
|
|
279
|
+
### Creation Options
|
|
272
280
|
|
|
273
|
-
`
|
|
281
|
+
The factory `opts` parameter is forwarded to the underlying SDK's `Sandbox.create(...)` call for
|
|
282
|
+
every fresh sandbox the framework creates — both the template at prewarm time and the session at
|
|
283
|
+
first-time session-create.
|
|
274
284
|
|
|
275
|
-
|
|
285
|
+
On resume (when the framework reattaches to an existing persistent session via `Sandbox.get`), no
|
|
286
|
+
`Sandbox.create` call happens, so the factory opts are not re-applied. The existing sandbox keeps
|
|
287
|
+
whatever configuration it had from its prior creation.
|
|
276
288
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
Lifecycle hooks remain update-time and override post-create:
|
|
290
|
+
|
|
291
|
+
- `bootstrap.use(opts)` calls the SDK's `sandbox.update(opts)` on the template, before the
|
|
292
|
+
snapshot is captured.
|
|
293
|
+
- `onSession.use(opts)` calls `sandbox.update(opts)` on the live session every time it opens.
|
|
294
|
+
|
|
295
|
+
So `vercelBackend({ networkPolicy: "deny-all" })` paired with no authored hooks puts every fresh
|
|
296
|
+
sandbox in deny-all mode. Adding `onSession({ use }) { await use({ networkPolicy: "allow-all" }); }`
|
|
297
|
+
overrides that for every session open.
|
|
298
|
+
|
|
299
|
+
The Vercel SDK forbids the `runtime` field when creating from a snapshot, so Ash strips `runtime`
|
|
300
|
+
from session-create calls. `runtime` only takes effect on the template create — the resulting
|
|
301
|
+
snapshot then determines the runtime for every session derived from it.
|
|
302
|
+
|
|
303
|
+
### Deferring Backend Construction
|
|
283
304
|
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
`SandboxDefinition.backend` also accepts a zero-arg callback that returns a backend. The callback
|
|
306
|
+
fires lazily on first framework access and the result is memoized for the lifetime of the process —
|
|
307
|
+
so backend-internal state (such as the Vercel backend's prewarmed-template cache) is preserved
|
|
308
|
+
across every call. Use this form to defer evaluation, for example when create options depend on
|
|
309
|
+
environment variables that aren't set at module load time:
|
|
286
310
|
|
|
287
311
|
```ts
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
resources: { vcpus: 4 },
|
|
292
|
-
timeout: 60 * 60 * 1_000,
|
|
293
|
-
});
|
|
294
|
-
// Can also update later:
|
|
295
|
-
await sandbox.update({ tags: { user: "casey" } });
|
|
296
|
-
}
|
|
312
|
+
export default defineSandbox({
|
|
313
|
+
backend: () => vercelBackend({ env: { TOKEN: process.env.TOKEN ?? "" } }),
|
|
314
|
+
});
|
|
297
315
|
```
|
|
298
316
|
|
|
299
|
-
This split ensures that `bootstrap` only does filesystem setup (which survives snapshotting), while
|
|
300
|
-
`onSession` configures the live session (network policy, resources, etc. do not survive snapshots).
|
|
301
|
-
|
|
302
317
|
### Network Policies
|
|
303
318
|
|
|
304
|
-
|
|
319
|
+
Three forms are supported on both the factory and the hook `use()` calls:
|
|
305
320
|
|
|
306
321
|
```ts
|
|
307
|
-
|
|
308
|
-
|
|
322
|
+
networkPolicy: "allow-all"; // default
|
|
323
|
+
networkPolicy: "deny-all"; // block all egress, including DNS
|
|
309
324
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
},
|
|
315
|
-
});
|
|
325
|
+
networkPolicy: {
|
|
326
|
+
allow: ["ai-gateway.vercel.sh", "*.github.com"],
|
|
327
|
+
subnets: { deny: ["10.0.0.0/8"] },
|
|
328
|
+
};
|
|
316
329
|
```
|
|
317
330
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
in `onSession`.
|
|
331
|
+
To apply the policy from sandbox creation onward — including during `bootstrap`'s commands —
|
|
332
|
+
pass it to the factory: `vercelBackend({ networkPolicy: "deny-all" })`. To override per-session
|
|
333
|
+
or to apply it only after the template has been built, set it in `onSession`'s `use()`. The
|
|
334
|
+
common pattern: leave the factory open so `bootstrap` can `git clone` and `npm install`, then
|
|
335
|
+
lock down via `onSession`.
|
|
321
336
|
|
|
322
337
|
### Timeout
|
|
323
338
|
|
|
324
339
|
The `@vercel/sandbox` SDK shuts down idle VMs after a configurable timeout (default 5 minutes). Ash
|
|
325
|
-
raises this default to **30 minutes** so the sandbox survives across workflow step boundaries.
|
|
326
|
-
override
|
|
340
|
+
raises this default to **30 minutes** so the sandbox survives across workflow step boundaries. Set
|
|
341
|
+
a different default on the factory, or override per-session in `onSession`:
|
|
327
342
|
|
|
328
343
|
```ts
|
|
344
|
+
// factory default — applies to every fresh sandbox
|
|
345
|
+
backend: vercelBackend({ timeout: 60 * 60 * 1_000 }); // 1 hour
|
|
346
|
+
|
|
347
|
+
// or override per-session
|
|
329
348
|
async onSession({ use }) {
|
|
330
|
-
await use({ timeout: 60 * 60 * 1_000 });
|
|
349
|
+
await use({ timeout: 60 * 60 * 1_000 });
|
|
331
350
|
}
|
|
332
351
|
```
|
|
333
352
|
|
|
@@ -342,9 +361,12 @@ Ash attaches Vercel Sandbox tags for runtime attribution:
|
|
|
342
361
|
- `channel` — the active channel adapter kind
|
|
343
362
|
- `sessionId` — the Ash session id
|
|
344
363
|
|
|
345
|
-
Custom tags can be set
|
|
364
|
+
Custom tags can be set on the factory (applied at every fresh `Sandbox.create`) or via
|
|
365
|
+
`onSession`'s `use()` (applied via `sandbox.update`):
|
|
346
366
|
|
|
347
367
|
```ts
|
|
368
|
+
backend: vercelBackend({ tags: { team: "infra" } });
|
|
369
|
+
// or
|
|
348
370
|
async onSession({ use }) {
|
|
349
371
|
await use({ tags: { team: "infra" } });
|
|
350
372
|
}
|
|
@@ -257,9 +257,9 @@ that history bounded:
|
|
|
257
257
|
|
|
258
258
|
### Bound your output inside `execute`
|
|
259
259
|
|
|
260
|
-
Whatever `execute` returns is
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
Whatever `execute` returns is what the model sees by default. (Use [`toModelOutput`](#tomodeloutput--controlling-what-the-model-sees) to project a
|
|
261
|
+
smaller summary instead.) If a tool can return unbounded text, cap it in the executor before
|
|
262
|
+
returning:
|
|
263
263
|
|
|
264
264
|
```ts
|
|
265
265
|
import { defineTool } from "experimental-ash/tools";
|
|
@@ -317,6 +317,58 @@ export default defineTool({
|
|
|
317
317
|
`retentionPolicy` runs every time an older result of this tool exits the protection window. The
|
|
318
318
|
`summary` text replaces the original tool-result output in history.
|
|
319
319
|
|
|
320
|
+
### `toModelOutput` — Controlling What The Model Sees
|
|
321
|
+
|
|
322
|
+
By default, the model sees the full return value of `execute`. When a tool returns rich structured
|
|
323
|
+
data that channels need for rendering but the model only needs a summary, use `toModelOutput` to
|
|
324
|
+
project the output:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { defineTool } from "experimental-ash/tools";
|
|
328
|
+
import { z } from "zod";
|
|
329
|
+
|
|
330
|
+
export default defineTool({
|
|
331
|
+
description: "Generate a structured account report.",
|
|
332
|
+
inputSchema: z.object({ domain: z.string() }),
|
|
333
|
+
async execute({ domain }) {
|
|
334
|
+
// Full payload — channels see all of this via action.result events.
|
|
335
|
+
return {
|
|
336
|
+
domain,
|
|
337
|
+
opportunityScore: 82,
|
|
338
|
+
angles: [
|
|
339
|
+
{ title: "API migration", rationale: "3x call growth" },
|
|
340
|
+
{ title: "Dashboard consolidation", rationale: "Multi-product usage" },
|
|
341
|
+
],
|
|
342
|
+
signals: [{ label: "3x API call growth", source: "usage" }],
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
// The model only sees a compact summary.
|
|
346
|
+
toModelOutput(output) {
|
|
347
|
+
return {
|
|
348
|
+
type: "text",
|
|
349
|
+
value: `Report for ${output.domain}: score ${output.opportunityScore}, ${output.angles.length} angles.`,
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`toModelOutput` receives the full `execute` return (strongly typed) and returns a `ToolModelOutput`:
|
|
356
|
+
|
|
357
|
+
| Shape | When to use |
|
|
358
|
+
| ------------------------------------ | ------------------------------------------- |
|
|
359
|
+
| `{ type: "text", value: string }` | The model should see a plain text summary. |
|
|
360
|
+
| `{ type: "json", value: JSONValue }` | The model should see a smaller JSON object. |
|
|
361
|
+
|
|
362
|
+
When `toModelOutput` is omitted, the model sees the full `execute` return as today.
|
|
363
|
+
|
|
364
|
+
Channel event handlers and hooks always receive the full `execute` return on `action.result`
|
|
365
|
+
events, regardless of whether `toModelOutput` is defined. This lets channels render rich
|
|
366
|
+
platform-specific output (e.g. Slack Block Kit) from structured tool data that the model never
|
|
367
|
+
needs to see.
|
|
368
|
+
|
|
369
|
+
Note: `retentionPolicy` and `onCompact` operate on the projected output (what the model sees),
|
|
370
|
+
not the full `execute` return.
|
|
371
|
+
|
|
320
372
|
## What To Read Next
|
|
321
373
|
|
|
322
374
|
- [Session Context](./session-context.md)
|
|
@@ -79,9 +79,14 @@ Channel and Slack types exported from `experimental-ash/channels/slack`:
|
|
|
79
79
|
`recentMessages`, `mentionUser`
|
|
80
80
|
- `SlackMessage` - parsed inbound mention or DM payload (`text`, `markdown`, `author`,
|
|
81
81
|
`attachments`, `threadTs`, `channelId`, ...)
|
|
82
|
+
- `loadThreadContextMessages(thread, message, options?)` - refreshes a Slack thread reply and
|
|
83
|
+
returns recent messages with the triggering message filtered out, or `[]` for thread roots. Pass
|
|
84
|
+
`since: "last-agent-reply"` to return only messages after the agent's last Slack reply, or
|
|
85
|
+
`since: (message) => boolean` for a custom boundary.
|
|
82
86
|
- `SlackInteractionAction` - action type for `onInteraction`
|
|
83
87
|
- `SlackMentionResult` / `SlackInboundResult` - return type of `onAppMention` / `onDirectMessage`
|
|
84
|
-
(`{ auth } | null`)
|
|
88
|
+
(`{ auth, modelContext? } | null`)
|
|
89
|
+
- `defaultSlackAuth(message, ctx)` - default Slack actor-to-session-auth projection
|
|
85
90
|
- `Card`, `Button`, `Actions`, `Section`, `Modal`, `Table`, etc. - card builders re-exported for
|
|
86
91
|
rendering Slack messages
|
|
87
92
|
|
|
@@ -13,10 +13,20 @@ const log = createLogger("channel.adapter");
|
|
|
13
13
|
*/
|
|
14
14
|
export function defaultDeliverResult(payload) {
|
|
15
15
|
if (payload.message !== undefined) {
|
|
16
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
inputResponses: payload.inputResponses,
|
|
18
|
+
message: payload.message,
|
|
19
|
+
modelContext: payload.modelContext,
|
|
20
|
+
};
|
|
17
21
|
}
|
|
18
22
|
if (payload.inputResponses !== undefined && payload.inputResponses.length > 0) {
|
|
19
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
inputResponses: payload.inputResponses,
|
|
25
|
+
modelContext: payload.modelContext,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (payload.modelContext !== undefined && payload.modelContext.length > 0) {
|
|
29
|
+
return { modelContext: payload.modelContext };
|
|
20
30
|
}
|
|
21
31
|
return undefined;
|
|
22
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UserContent } from "ai";
|
|
1
|
+
import type { ModelMessage, UserContent } from "ai";
|
|
2
2
|
import type { CrossChannelReceiveFn } from "#channel/cross-channel-receive.js";
|
|
3
3
|
import type { SessionAuthContext } from "#channel/types.js";
|
|
4
4
|
import type { InputResponse } from "#runtime/input/types.js";
|
|
@@ -21,6 +21,14 @@ export interface RouteHandlerArgs<TState = undefined> {
|
|
|
21
21
|
export interface SendPayload {
|
|
22
22
|
readonly message?: string | UserContent;
|
|
23
23
|
readonly inputResponses?: readonly InputResponse[];
|
|
24
|
+
/**
|
|
25
|
+
* Ephemeral messages contributed by the channel, appended to the
|
|
26
|
+
* dispatched turn's first model call via `StepInput.modelContext`.
|
|
27
|
+
* Never persisted to durable session history. Use `role: "system"`
|
|
28
|
+
* for background context that should land before the user message;
|
|
29
|
+
* other roles land after the user message.
|
|
30
|
+
*/
|
|
31
|
+
readonly modelContext?: readonly ModelMessage[];
|
|
24
32
|
}
|
|
25
33
|
export type SendFn<TState = undefined> = (input: string | UserContent | SendPayload, options: SendOptions<TState>) => Promise<Session>;
|
|
26
34
|
/**
|
package/dist/src/channel/send.js
CHANGED
|
@@ -10,13 +10,13 @@ export function createSendFn(runtime, adapter, channelName) {
|
|
|
10
10
|
const state = options.state;
|
|
11
11
|
const rawToken = options.continuationToken;
|
|
12
12
|
const continuationToken = `${channelName}:${rawToken}`;
|
|
13
|
-
const { message: rawMessage, inputResponses } = normalizeSendInput(input);
|
|
13
|
+
const { message: rawMessage, inputResponses, modelContext } = normalizeSendInput(input);
|
|
14
14
|
const message = serializeUrlFilePartsInMessage(rawMessage);
|
|
15
15
|
try {
|
|
16
16
|
const { sessionId } = await runtime.deliver({
|
|
17
17
|
auth,
|
|
18
18
|
continuationToken,
|
|
19
|
-
payload: { inputResponses, message },
|
|
19
|
+
payload: { inputResponses, message, modelContext },
|
|
20
20
|
});
|
|
21
21
|
return createSession(sessionId, rawToken, runtime);
|
|
22
22
|
}
|
|
@@ -42,7 +42,7 @@ export function createSendFn(runtime, adapter, channelName) {
|
|
|
42
42
|
auth,
|
|
43
43
|
capabilities: { requestInput: true },
|
|
44
44
|
continuationToken,
|
|
45
|
-
input: { message: message ?? "" },
|
|
45
|
+
input: { message: message ?? "", modelContext },
|
|
46
46
|
mode: "conversation",
|
|
47
47
|
});
|
|
48
48
|
return createSession(handle.sessionId, rawToken, runtime);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UserContent } from "ai";
|
|
1
|
+
import type { ModelMessage, UserContent } from "ai";
|
|
2
2
|
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
3
3
|
import type { RunMode } from "#run-mode.js";
|
|
4
4
|
import type { RuntimeActionResult } from "#runtime/actions/types.js";
|
|
@@ -59,6 +59,7 @@ export type EventEmitFn = (event: HandleMessageStreamEvent) => Promise<void>;
|
|
|
59
59
|
export interface DeliverPayload {
|
|
60
60
|
readonly inputResponses?: readonly InputResponse[];
|
|
61
61
|
readonly message?: string | UserContent;
|
|
62
|
+
readonly modelContext?: readonly ModelMessage[];
|
|
62
63
|
readonly [key: string]: unknown;
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
@@ -177,6 +178,7 @@ export interface RunInput {
|
|
|
177
178
|
readonly initiatorAuth?: SessionAuthContext | null;
|
|
178
179
|
readonly input: {
|
|
179
180
|
readonly message: string | UserContent;
|
|
181
|
+
readonly modelContext?: readonly ModelMessage[];
|
|
180
182
|
};
|
|
181
183
|
readonly mode: RunMode;
|
|
182
184
|
readonly parent?: SessionParent;
|