experimental-ash 0.34.0 → 0.36.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 +6 -5
- package/dist/docs/internals/core-beliefs.md +2 -2
- package/dist/docs/internals/hooks.md +16 -11
- package/dist/docs/internals/mechanical-invariants.md +4 -4
- package/dist/docs/internals/testing.md +1 -1
- package/dist/docs/public/README.md +1 -1
- package/dist/docs/public/auth-and-route-protection.md +12 -3
- package/dist/docs/public/channels/{README.md → index.md} +2 -2
- package/dist/docs/public/channels/slack.md +3 -3
- package/dist/docs/public/cli-build-and-debugging.md +1 -1
- package/dist/docs/public/faqs.md +4 -5
- package/dist/docs/public/hooks.md +18 -23
- package/dist/docs/public/meta.json +4 -4
- package/dist/docs/public/sandbox.md +51 -30
- package/dist/docs/public/schedules.md +1 -1
- package/dist/docs/public/session-context.md +26 -28
- package/dist/docs/public/skills.md +3 -4
- package/dist/docs/public/subagents.md +1 -1
- package/dist/docs/public/tools.md +13 -17
- package/dist/docs/public/typescript-api.md +10 -11
- package/dist/src/channel/session.d.ts +3 -29
- package/dist/src/channel/session.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +2 -2
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +11 -2
- package/dist/src/compiled/@vercel/sandbox/index.js +3 -3
- package/dist/src/compiled/@vercel/sandbox/package.json +1 -1
- package/dist/src/compiled/_chunks/node/{auth-ZhCJAHxl.js → auth-CVVvWjaK.js} +1 -1
- package/dist/src/compiled/_chunks/node/{version-D4IYmfaS.js → version-nR4RSpFw.js} +1 -1
- package/dist/src/context/build-callback-context.d.ts +8 -0
- package/dist/src/context/build-callback-context.js +1 -0
- package/dist/src/context/hook-lifecycle.js +1 -1
- package/dist/src/execution/node-step.js +1 -1
- package/dist/src/execution/sandbox/bash-tool.d.ts +2 -1
- package/dist/src/execution/sandbox/bash-tool.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
- package/dist/src/execution/sandbox/glob-tool.d.ts +2 -1
- package/dist/src/execution/sandbox/glob-tool.js +3 -3
- package/dist/src/execution/sandbox/grep-tool.d.ts +2 -1
- package/dist/src/execution/sandbox/grep-tool.js +3 -3
- package/dist/src/execution/sandbox/read-file-tool.d.ts +2 -1
- package/dist/src/execution/sandbox/read-file-tool.js +1 -1
- package/dist/src/execution/sandbox/session.js +1 -1
- package/dist/src/execution/sandbox/write-file-tool.d.ts +2 -1
- package/dist/src/execution/sandbox/write-file-tool.js +1 -1
- package/dist/src/execution/tool-compaction.js +1 -1
- package/dist/src/harness/code-mode-approval.js +1 -1
- package/dist/src/harness/code-mode.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/logging.js +1 -1
- package/dist/src/internal/workflow-bundle/builder.js +2 -2
- package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/index.js +1 -1
- package/dist/src/node_modules/.pnpm/experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_/node_modules/experimental-ai-sdk-code-mode/dist/runtime/manager.js +1 -0
- package/dist/src/node_modules/.pnpm/experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_/node_modules/experimental-ai-sdk-code-mode/dist/runtime/worker-source.js +408 -0
- package/dist/src/packages/ash-scaffold/src/channels.js +2 -12
- package/dist/src/packages/ash-scaffold/src/pnpm-workspace.js +11 -0
- package/dist/src/packages/ash-scaffold/src/project.js +1 -1
- package/dist/src/packages/ash-scaffold/src/web-template.js +2 -2
- package/dist/src/public/channels/auth.d.ts +22 -11
- package/dist/src/public/channels/auth.js +1 -1
- package/dist/src/public/context/index.d.ts +3 -2
- package/dist/src/public/context/index.js +1 -1
- package/dist/src/public/definitions/callback-context.d.ts +22 -0
- package/dist/src/public/definitions/callback-context.js +1 -0
- package/dist/src/public/definitions/hook.d.ts +9 -49
- package/dist/src/public/definitions/sandbox.d.ts +1 -1
- package/dist/src/public/definitions/tool.d.ts +14 -15
- package/dist/src/public/hooks/index.d.ts +1 -1
- package/dist/src/public/sandbox/index.d.ts +1 -2
- package/dist/src/public/sandbox/index.js +1 -1
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +4 -4
- package/dist/src/public/skills/index.d.ts +0 -1
- package/dist/src/public/skills/index.js +1 -1
- package/dist/src/public/tools/defaults.js +1 -1
- package/dist/src/public/tools/define-bash-tool.js +1 -1
- package/dist/src/public/tools/define-glob-tool.js +1 -1
- package/dist/src/public/tools/define-grep-tool.js +1 -1
- package/dist/src/public/tools/define-read-file-tool.js +1 -1
- package/dist/src/public/tools/define-write-file-tool.js +1 -1
- package/dist/src/public/tools/index.d.ts +2 -1
- package/dist/src/public/tools/internal.d.ts +4 -0
- package/dist/src/public/tools/internal.js +1 -1
- package/dist/src/runtime/framework-tools/bash.js +1 -1
- package/dist/src/runtime/framework-tools/connection-search.js +1 -1
- package/dist/src/runtime/framework-tools/connection-tools.js +1 -1
- package/dist/src/runtime/framework-tools/file-state.d.ts +2 -2
- package/dist/src/runtime/framework-tools/file-state.js +1 -1
- package/dist/src/runtime/framework-tools/glob.js +1 -1
- package/dist/src/runtime/framework-tools/grep.js +1 -1
- package/dist/src/runtime/framework-tools/read-file.js +2 -2
- package/dist/src/runtime/framework-tools/todo.js +1 -1
- package/dist/src/runtime/framework-tools/write-file.js +1 -1
- package/dist/src/runtime/governance/auth/oidc.js +1 -1
- package/dist/src/runtime/governance/auth/token-claims.d.ts +2 -0
- package/dist/src/runtime/governance/auth/token-claims.js +1 -1
- package/dist/src/runtime/governance/auth/types.d.ts +6 -0
- package/dist/src/runtime/types.d.ts +2 -2
- package/dist/src/shared/sandbox-session.d.ts +0 -17
- package/package.json +3 -3
- package/dist/src/node_modules/.pnpm/experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_/node_modules/experimental-ai-sdk-code-mode/dist/runtime/manager.js +0 -1
- package/dist/src/node_modules/.pnpm/experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_/node_modules/experimental-ai-sdk-code-mode/dist/runtime/worker-source.js +0 -1153
- package/dist/src/node_modules/.pnpm/experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_/node_modules/experimental-ai-sdk-code-mode/dist/runtime-assets.js +0 -1
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/approval-continuation.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/approval-response.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/code-mode-tool.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/continuation-capability.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/errors.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/fetch-policy.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/host-interrupt.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/interrupt-continuation.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/options.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/run-code-mode.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/runtime/max-workers.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/serialization.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/source-cache.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/telemetry.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/tool-invocation.js +0 -0
- /package/dist/src/node_modules/.pnpm/{experimental-ai-sdk-code-mode@1.0.9_ai@7.0.0-canary.154_zod@4.4.3_ → experimental-ai-sdk-code-mode@1.0.10_ai@7.0.0-canary.154_zod@4.4.3_}/node_modules/experimental-ai-sdk-code-mode/dist/tool-prompt.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.36.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 34722b0: Unify runtime data access: `ctx` is available as the last argument to all ALS-scoped authored callbacks. Omit it when you don't need it.
|
|
8
|
+
|
|
9
|
+
**Breaking changes:**
|
|
10
|
+
|
|
11
|
+
- Tool `execute(input)` → `execute(input, ctx)` — access session, sandbox, and skills via `ctx`
|
|
12
|
+
- Hook lifecycle `(input, ctx)` → `(ctx)` — session identity, auth, and turn info are on `ctx.session`
|
|
13
|
+
- Hook/channel events `(event, ctx)` → same shape, but `ctx` now carries `session`, `getSandbox()`, `getSkill()`
|
|
14
|
+
- `SessionHandle.id` (unchanged), `.auth` is now `{ current, initiator }` (was flat)
|
|
15
|
+
- `getSession()`, `getSandbox()`, `getSkill()` removed — use `ctx.session`, `ctx.getSandbox()`, `ctx.getSkill()`
|
|
16
|
+
- `HookContext.ash` removed — use `defineState` for durable state
|
|
17
|
+
- `CompactionHookInput` → `onCompact({ history, signal }, ctx)`
|
|
18
|
+
- `LifecycleHookInput` removed (absorbed into `HookContext`)
|
|
19
|
+
|
|
20
|
+
Non-ALS callbacks unchanged: schedule `run`, sandbox `bootstrap`/`onSession`, instrumentation `setup`.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 4e50396: Fixed parked subagent/remote-agent turns persisting a stale (default) emission state. The runtime-action park now stamps the live turn's emission identity onto the parked session, so the resume turn is classified as a continuation instead of a fresh turn — preventing duplicate `session.started`/`turn.started` events and empty `turnId`s on resume.
|
|
25
|
+
|
|
26
|
+
## 0.35.0
|
|
27
|
+
|
|
28
|
+
### Minor Changes
|
|
29
|
+
|
|
30
|
+
- 24aa0cd: `localDev()` now also grants the local-dev session when running under `vercel dev` (`VERCEL=1` and `VERCEL_ENV=development`), not just loopback requests, so the local Vercel dev server can reach the agent without an OIDC token. `ASH_LOG_LEVEL` now defaults to `info` in every environment (previously `debug` outside production), making `debug` opt-in so dev output is no longer flooded with best-effort lines.
|
|
31
|
+
- a59d91c: Remove deprecated `SandboxSession.runCommand` alias and `SandboxRunCommandOptions` type. Use `sandbox.run({ command })` and `SandboxRunOptions` instead.
|
|
32
|
+
- 083c20a: `vercelOidc()` now accepts Vercel OIDC tokens with `external_sub` as user principals when the token matches the configured Vercel project and deployment environment. The resulting session auth uses `external_sub` as the subject, prefers `external_iss` / `connector_id` as the issuer, and exposes string OIDC profile claims such as `name`, `picture`, and `email` as attributes.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 321bae2: Upgrade `@vercel/sandbox` from 2.0.0 to 2.0.1. Sessions are now persistent by default and auto-resume on `Sandbox.get()`. Vendored types updated with `resume`, `onResume`, and `readFile`.
|
|
37
|
+
|
|
3
38
|
## 0.34.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
|
@@ -17,7 +17,7 @@ interface AshContext {
|
|
|
17
17
|
}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`loadContext()` returns the active context or throws.
|
|
20
|
+
`loadContext()` returns the active context or throws. Internal accessors (`getSession()`, `getSandbox()`, `getSkill()`) delegate to it — these are implementation details, not public exports. Authored code uses `ctx.session`, `ctx.getSandbox()`, and `ctx.getSkill()` instead.
|
|
21
21
|
|
|
22
22
|
## Type Hierarchy
|
|
23
23
|
|
|
@@ -78,9 +78,10 @@ Framework providers use a superset `FrameworkContextProvider` that also receives
|
|
|
78
78
|
5. Call framework provider `commit` hooks for provider-owned session state
|
|
79
79
|
|
|
80
80
|
Authored hook lifecycle dispatch (`lifecycle.session`, `lifecycle.turn`)
|
|
81
|
-
runs inside step (4)'s ALS scope, before the harness step.
|
|
82
|
-
|
|
83
|
-
`SessionPreparedKey`
|
|
81
|
+
runs inside step (4)'s ALS scope, before the harness step. Lifecycle
|
|
82
|
+
hooks receive `(ctx)`, and event hooks receive `(event, ctx)`. See
|
|
83
|
+
[Hooks](./hooks.md) for the full pipeline and the `SessionPreparedKey`
|
|
84
|
+
flag's failure semantics.
|
|
84
85
|
|
|
85
86
|
`StepInput.modelContext` has two producers. Channels can provide
|
|
86
87
|
ephemeral messages through `SendPayload.modelContext`, and lifecycle
|
|
@@ -91,7 +92,7 @@ call only and are never persisted to durable session history.
|
|
|
91
92
|
|
|
92
93
|
## Channel Context
|
|
93
94
|
|
|
94
|
-
Channels set custom durable context keys inside `onDeliver(ctx, payload)`. The method receives a
|
|
95
|
+
Channels set custom durable context keys inside `onDeliver(ctx, payload)`. The method receives a context accessor 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.
|
|
95
96
|
|
|
96
97
|
Auth lives on the run and deliver inputs:
|
|
97
98
|
|
|
@@ -15,8 +15,8 @@ source of truth; a repeated `name` field is a second source that can drift.
|
|
|
15
15
|
|
|
16
16
|
The hooks surface (`agent/hooks/`) deliberately reuses the unified
|
|
17
17
|
`AshContext` rather than introducing a parallel state-patch channel:
|
|
18
|
-
hooks read and write through `ctx
|
|
19
|
-
|
|
18
|
+
hooks read and write through `ctx` like every other authored function.
|
|
19
|
+
One context, one set of keys, one place to look when debugging.
|
|
20
20
|
|
|
21
21
|
## Durable Work Belongs In The Runtime
|
|
22
22
|
|
|
@@ -18,15 +18,20 @@ through one runtime registry, one `HookContext`, one set of ordering
|
|
|
18
18
|
rules.
|
|
19
19
|
|
|
20
20
|
The hooks surface deliberately reuses the unified `AshContext`:
|
|
21
|
-
`HookContext
|
|
22
|
-
channels get. There is no
|
|
23
|
-
`AshContext`, one set of keys, one
|
|
21
|
+
`HookContext` extends `AshCallbackContext`, giving hooks the same
|
|
22
|
+
context surface tools, providers, and channels get. There is no
|
|
23
|
+
parallel state-patch channel — one `AshContext`, one set of keys, one
|
|
24
|
+
place to look when debugging.
|
|
24
25
|
|
|
25
|
-
`HookContext`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
`HookContext` carries `agent`, `channel`, and `session` (which includes
|
|
27
|
+
`id`, `auth`, `turn`, and `parent`). `session.auth` is a nested
|
|
28
|
+
`SessionAuth` with `{ current, initiator }`.
|
|
29
|
+
|
|
30
|
+
Lifecycle hooks receive `(ctx)` — a single `HookContext` argument.
|
|
31
|
+
Event hooks receive `(event, ctx)` — event first, context last (omit `ctx` when unused).
|
|
32
|
+
Per-turn `turnId` and `sequence` come from `ctx.session.turn` for
|
|
33
|
+
lifecycle hooks, and from `event.data.{turnId, sequence}` for
|
|
34
|
+
stream-event hooks — never duplicated on the context.
|
|
30
35
|
|
|
31
36
|
## Authoring shape
|
|
32
37
|
|
|
@@ -87,7 +92,7 @@ the workflow path.
|
|
|
87
92
|
Lifecycle dispatch lives in `src/context/hook-lifecycle.ts`. The
|
|
88
93
|
workflow runtime entry point (`execution/workflow-steps.ts`) calls
|
|
89
94
|
`runHookLifecycleStep` once per turn, **inside** the active ALS scope
|
|
90
|
-
so hook code can read and write
|
|
95
|
+
so hook code can read and write context like every other authored
|
|
91
96
|
function. `runHookLifecycleStep` wraps `dispatchHookLifecycle`, lowers
|
|
92
97
|
a `turn-failed` outcome into a parking `StepResult` (`{ next: null }`
|
|
93
98
|
for conversation mode, `{ next: { done: true, output } }` for task
|
|
@@ -142,7 +147,7 @@ Subagent hooks are fully isolated by structural construction:
|
|
|
142
147
|
established by the existing subagent invocation pipeline. Hooks
|
|
143
148
|
resolve from the subagent node's `hookRegistry`, not the parent
|
|
144
149
|
agent's.
|
|
145
|
-
- `ctx
|
|
150
|
+
- `ctx` reads hit the subagent's container, not the parent's.
|
|
146
151
|
- Lexicographic ordering applies within a subagent's hook set only.
|
|
147
152
|
Parent and child registries do not interleave.
|
|
148
153
|
|
|
@@ -188,7 +193,7 @@ documented expectations:
|
|
|
188
193
|
- Hook modules never import workflow primitives (`start`, `resumeHook`,
|
|
189
194
|
`createHook`, `getWritable`).
|
|
190
195
|
- Hook return shapes have no `state` field — durable state goes through
|
|
191
|
-
`ctx
|
|
196
|
+
`ctx`.
|
|
192
197
|
- Subagent hooks never read parent ALS context.
|
|
193
198
|
|
|
194
199
|
## Related Pages
|
|
@@ -68,10 +68,10 @@ flags new `@workflow/*` imports from channel, harness, and hook code.
|
|
|
68
68
|
### Hooks Use The Single State Channel
|
|
69
69
|
|
|
70
70
|
Authored hook modules (`agent/hooks/**`) must read and write durable state
|
|
71
|
-
through `ctx
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
through `ctx` (the unified context). The hooks surface introduces no
|
|
72
|
+
parallel state-patch channel — lifecycle return shapes expose only
|
|
73
|
+
`modelContext` (a one-shot per-call ephemeral message list), and subagent
|
|
74
|
+
hooks never read parent ALS context.
|
|
75
75
|
|
|
76
76
|
Enforcement: rule 27 in `scripts/guard-agents-rules.mjs` fails if a
|
|
77
77
|
`state:` (or `readonly state:`, `state?:`) field is declared on any
|
|
@@ -62,7 +62,7 @@ Ad-hoc tmp directories with `package.json` + `agent/` scaffolding.
|
|
|
62
62
|
|
|
63
63
|
- Unit tier: `unit-guard.ts` throws on disk writes, subprocesses, `process.chdir`, real fetch. If you hit a guard throw, move the test to a higher tier.
|
|
64
64
|
- Never mutate the process-default `RuntimeSession` — let `createTestRuntime().run(fn)` scope it. Do not call `installBundledCompiledArtifacts` / `resetBundledCompiledArtifacts` / `clearProcessDefaultRuntimeSession` directly.
|
|
65
|
-
- Use `runAsSession({}, ...)` when the test needs `
|
|
65
|
+
- Use `runAsSession({}, ...)` when the test needs `ctx.session` / `ctx.getSandbox()` / `ctx.getSkill()` to resolve (the internal `getSession()`, `getSandbox()`, `getSkill()` free functions still work but are not public API). Use `run(...)` when only compiled artifacts + bundle cache need scoping.
|
|
66
66
|
- Cleanup is automatic for `mockSkill`, `useScenarioApp`, and `useTemporaryAppRoots`.
|
|
67
67
|
- Do not commit new fixtures under `test/fixtures/` — CI guard will fail. Use descriptors in `src/internal/testing/scenario-apps/`.
|
|
68
68
|
- Prefer `vi.stubEnv` over direct `process.env` mutation.
|
|
@@ -60,7 +60,7 @@ Ash then gives you:
|
|
|
60
60
|
- a reconnectable session stream
|
|
61
61
|
- durable session state across turns
|
|
62
62
|
- a per-agent sandbox with a shared runtime workspace
|
|
63
|
-
- typed runtime helpers
|
|
63
|
+
- typed runtime helpers accessed through `ctx` (`ctx.session`, `ctx.getSandbox()`, `ctx.getSkill()`)
|
|
64
64
|
|
|
65
65
|
## The Runtime Shape
|
|
66
66
|
|
|
@@ -156,6 +156,15 @@ Use this for the common Vercel deployment path. Verifies a bearer JWT against th
|
|
|
156
156
|
issuer; tokens minted for the current `VERCEL_PROJECT_ID` are always accepted (so internal
|
|
157
157
|
subagent / runtime callers authenticate without configuration).
|
|
158
158
|
|
|
159
|
+
It accepts Vercel OIDC bearer tokens after signature, issuer, audience, and time-claim verification.
|
|
160
|
+
Tokens for the current `VERCEL_PROJECT_ID` authenticate as runtime/service callers. Tokens with
|
|
161
|
+
`external_sub` authenticate as user callers when their `project_id` matches `VERCEL_PROJECT_ID` if
|
|
162
|
+
configured, and their `environment` matches `VERCEL_TARGET_ENV` or `VERCEL_ENV` if configured. For
|
|
163
|
+
those user tokens, `external_sub` becomes the session subject, `external_iss` becomes the session
|
|
164
|
+
issuer when present, and `connector_id` is used as the issuer fallback before the Vercel OIDC issuer.
|
|
165
|
+
String OIDC profile claims such as `name`, `picture`, and `email` are exposed in
|
|
166
|
+
`ctx.session.auth.current.attributes`.
|
|
167
|
+
|
|
159
168
|
### `none`
|
|
160
169
|
|
|
161
170
|
Returns a synthetic anonymous `SessionAuthContext`. Use as the final entry in `auth` to accept
|
|
@@ -186,7 +195,7 @@ re-materializes them from the authored channel definition when it needs the live
|
|
|
186
195
|
|
|
187
196
|
## What Auth Reaches Authored Code
|
|
188
197
|
|
|
189
|
-
Inside runtime code, `
|
|
198
|
+
Inside runtime code, `ctx.session.auth` gives you the caller snapshot.
|
|
190
199
|
|
|
191
200
|
Important behavior:
|
|
192
201
|
|
|
@@ -198,8 +207,8 @@ Important behavior:
|
|
|
198
207
|
accepts with a `SessionAuthContext` or returns `401`
|
|
199
208
|
|
|
200
209
|
Auth on follow-up messages (`deliver()`) is honored by the workflow runtime. When a different
|
|
201
|
-
user sends a follow-up to the same session, `session.auth.current` reflects the new caller on
|
|
202
|
-
that turn while `session.auth.initiator` stays the same.
|
|
210
|
+
user sends a follow-up to the same session, `ctx.session.auth.current` reflects the new caller on
|
|
211
|
+
that turn while `ctx.session.auth.initiator` stays the same.
|
|
203
212
|
|
|
204
213
|
## What Ash Does Not Do
|
|
205
214
|
|
|
@@ -179,7 +179,7 @@ export default slackChannel({
|
|
|
179
179
|
if (event.finishReason === "tool-calls") return;
|
|
180
180
|
if (event.message) ctx.thread.post(event.message);
|
|
181
181
|
},
|
|
182
|
-
"session.failed"(
|
|
182
|
+
"session.failed"(_event, ctx) {
|
|
183
183
|
ctx.thread.post("Something went wrong.");
|
|
184
184
|
},
|
|
185
185
|
},
|
|
@@ -372,7 +372,7 @@ Semantics:
|
|
|
372
372
|
|
|
373
373
|
- The target channel's authored `receive(input, { send })` hook owns the continuation-token
|
|
374
374
|
format and the initial state. Callers supply only `{ message, args, auth }`.
|
|
375
|
-
- `auth` flows through to `session.
|
|
375
|
+
- `auth` flows through to `session.auth.initiator` so the target's event handlers and the
|
|
376
376
|
agent's tools can read who started the session.
|
|
377
377
|
- Calling `args.receive(...)` does **not** also start a session on the current channel. The
|
|
378
378
|
inbound channel's response is whatever the route handler returns explicitly (e.g.
|
|
@@ -163,7 +163,7 @@ export default slackChannel({
|
|
|
163
163
|
if (event.finishReason === "tool-calls") return;
|
|
164
164
|
if (event.message) ctx.thread.post(event.message);
|
|
165
165
|
},
|
|
166
|
-
"session.failed"(
|
|
166
|
+
"session.failed"(_event, ctx) {
|
|
167
167
|
ctx.thread.post("Something went wrong.");
|
|
168
168
|
},
|
|
169
169
|
},
|
|
@@ -194,8 +194,8 @@ export default slackChannel({
|
|
|
194
194
|
webhook side via `waitUntil`, so the channel returns `200 OK` to Slack immediately.
|
|
195
195
|
|
|
196
196
|
`events: { ... }` handlers receive runtime events emitted by the harness after the turn dispatches
|
|
197
|
-
(`turn.started`, `message.completed`, `session.failed`, etc.). They
|
|
198
|
-
not on the inbound webhook side.
|
|
197
|
+
(`turn.started`, `message.completed`, `session.failed`, etc.). They receive `(eventData, ctx)` and
|
|
198
|
+
run inside the workflow context, not on the inbound webhook side.
|
|
199
199
|
|
|
200
200
|
### Thread Context
|
|
201
201
|
|
|
@@ -77,7 +77,7 @@ When something is wrong:
|
|
|
77
77
|
|
|
78
78
|
- `schedules/` are root-only today
|
|
79
79
|
- `agent.ts` no longer owns inbound auth or network policy; those live on the HTTP channel layer
|
|
80
|
-
- runtime helpers
|
|
80
|
+
- runtime helpers on `ctx` (like `ctx.session`) do not work at module top level
|
|
81
81
|
- skill guidance belongs in `skills/`, not in tool descriptions
|
|
82
82
|
|
|
83
83
|
## What To Read Next
|
package/dist/docs/public/faqs.md
CHANGED
|
@@ -56,12 +56,11 @@ the framework's mechanical invariants.
|
|
|
56
56
|
|
|
57
57
|
If you want session data from your own code, there are two supported surfaces:
|
|
58
58
|
|
|
59
|
-
- **Tools** can read the current session's metadata (
|
|
60
|
-
parent lineage) via `
|
|
59
|
+
- **Tools** can read the current session's metadata (id, turn, auth,
|
|
60
|
+
parent lineage) via `ctx.session` from the optional `ctx` parameter on `execute`. This is
|
|
61
61
|
read-only.
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
[Session Context](./session-context.md).
|
|
62
|
+
- **`defineState`** is the supported pattern for reading or writing session-scoped durable state
|
|
63
|
+
from authored code — see [Session Context](./session-context.md).
|
|
65
64
|
|
|
66
65
|
For the wire-level session lifecycle (creating sessions, streaming events,
|
|
67
66
|
reconnecting by event index) see [Sessions And Streaming](./runs-and-streaming.md).
|
|
@@ -20,7 +20,7 @@ import { defineHook } from "experimental-ash/hooks";
|
|
|
20
20
|
export default defineHook({
|
|
21
21
|
events: {
|
|
22
22
|
async "session.started"(_event, ctx) {
|
|
23
|
-
console.info("session started", { sessionId: ctx.session.
|
|
23
|
+
console.info("session started", { sessionId: ctx.session.id });
|
|
24
24
|
},
|
|
25
25
|
async "message.completed"(event) {
|
|
26
26
|
console.info("model finished", { length: event.data.message?.length ?? 0 });
|
|
@@ -33,8 +33,7 @@ The slug is the path-relative basename: `agent/hooks/audit.ts` → `"audit"`,
|
|
|
33
33
|
`agent/hooks/auth/load-profile.ts` → `"auth/load-profile"`.
|
|
34
34
|
|
|
35
35
|
`defineHook`, `HookDefinition`, and `HookContext` live on
|
|
36
|
-
`experimental-ash/hooks`.
|
|
37
|
-
`getContext`, …) live on `experimental-ash/context`.
|
|
36
|
+
`experimental-ash/hooks`. The `defineState` helper lives on `experimental-ash/context`.
|
|
38
37
|
|
|
39
38
|
## Shape
|
|
40
39
|
|
|
@@ -55,16 +54,10 @@ Every handler receives the same `HookContext`:
|
|
|
55
54
|
interface HookContext {
|
|
56
55
|
readonly agent: { readonly name: string; readonly nodeId?: string };
|
|
57
56
|
readonly channel: { readonly kind?: string; readonly continuationToken?: string };
|
|
58
|
-
readonly session: { readonly
|
|
59
|
-
readonly ash: ContextAccessor;
|
|
57
|
+
readonly session: { readonly id: string };
|
|
60
58
|
}
|
|
61
59
|
```
|
|
62
60
|
|
|
63
|
-
`ctx.ash` is the same `ContextAccessor` tools, providers, and channels use.
|
|
64
|
-
Hooks read and write durable state through `ctx.ash.set(...)` /
|
|
65
|
-
`ctx.ash.get(...)`. The hooks surface deliberately introduces no parallel
|
|
66
|
-
state-patch channel — one `AshContext`, one set of keys.
|
|
67
|
-
|
|
68
61
|
### Narrowing tool results
|
|
69
62
|
|
|
70
63
|
`toolResultFrom` narrows an `action.result` event to a specific authored
|
|
@@ -108,11 +101,13 @@ Lifecycle hooks share one signature:
|
|
|
108
101
|
|
|
109
102
|
```ts
|
|
110
103
|
type LifecycleHook = (
|
|
111
|
-
input: { session: { sessionId: string }; turn: { sequence: number; turnId: string } },
|
|
112
104
|
ctx: HookContext,
|
|
113
105
|
) => void | { modelContext?: readonly ModelMessage[]; skills?: readonly NamedSkillDefinition[] } | Promise<…>;
|
|
114
106
|
```
|
|
115
107
|
|
|
108
|
+
Session and turn metadata are available on `ctx.session`. For example,
|
|
109
|
+
`ctx.session.turn.sequence` gives the current turn sequence number.
|
|
110
|
+
|
|
116
111
|
`lifecycle.session` runs **once per durable session**, before the first
|
|
117
112
|
model turn. `lifecycle.turn` runs **once per fresh delivery** — tool-loop
|
|
118
113
|
continuations and HITL resumes do not re-fire it.
|
|
@@ -130,24 +125,24 @@ lifecycle hook messages.
|
|
|
130
125
|
|
|
131
126
|
```ts
|
|
132
127
|
// agent/hooks/load-profile.ts
|
|
133
|
-
import {
|
|
128
|
+
import { defineState } from "experimental-ash/context";
|
|
134
129
|
import { defineHook } from "experimental-ash/hooks";
|
|
135
130
|
import { fetchGithubProfile, type GithubProfile } from "../lib/github";
|
|
136
131
|
|
|
137
|
-
const
|
|
132
|
+
const githubProfile = defineState<GithubProfile | null>("myapp.githubProfile", () => null);
|
|
138
133
|
|
|
139
134
|
export default defineHook({
|
|
140
135
|
lifecycle: {
|
|
141
|
-
async session(
|
|
142
|
-
const profile = await fetchGithubProfile(ctx.session.
|
|
143
|
-
|
|
136
|
+
async session(ctx) {
|
|
137
|
+
const profile = await fetchGithubProfile(ctx.session.id);
|
|
138
|
+
githubProfile.update(() => profile);
|
|
144
139
|
},
|
|
145
140
|
},
|
|
146
141
|
});
|
|
147
142
|
```
|
|
148
143
|
|
|
149
144
|
The profile is now visible to every tool, provider, and subsequent hook
|
|
150
|
-
through `
|
|
145
|
+
through `githubProfile.get()`.
|
|
151
146
|
|
|
152
147
|
### Inject ephemeral system text
|
|
153
148
|
|
|
@@ -158,7 +153,7 @@ import { fetchActiveIncident } from "../lib/linear";
|
|
|
158
153
|
|
|
159
154
|
export default defineHook({
|
|
160
155
|
lifecycle: {
|
|
161
|
-
async turn() {
|
|
156
|
+
async turn(ctx) {
|
|
162
157
|
const incident = await fetchActiveIncident();
|
|
163
158
|
if (incident === null) return;
|
|
164
159
|
|
|
@@ -186,7 +181,7 @@ import { defineHook } from "experimental-ash/hooks";
|
|
|
186
181
|
|
|
187
182
|
export default defineHook({
|
|
188
183
|
lifecycle: {
|
|
189
|
-
async session() {
|
|
184
|
+
async session(ctx) {
|
|
190
185
|
return {
|
|
191
186
|
skills: [
|
|
192
187
|
{
|
|
@@ -216,12 +211,12 @@ import { defineHook } from "experimental-ash/hooks";
|
|
|
216
211
|
|
|
217
212
|
export default defineHook({
|
|
218
213
|
lifecycle: {
|
|
219
|
-
async session(
|
|
214
|
+
async session(ctx) {
|
|
220
215
|
return {
|
|
221
216
|
modelContext: [
|
|
222
217
|
{
|
|
223
218
|
role: "system",
|
|
224
|
-
content: `Greet the user by name (${await lookupName(ctx.session.
|
|
219
|
+
content: `Greet the user by name (${await lookupName(ctx.session.id)}).`,
|
|
225
220
|
},
|
|
226
221
|
],
|
|
227
222
|
};
|
|
@@ -247,7 +242,7 @@ import { postToHoneycomb } from "../lib/honeycomb";
|
|
|
247
242
|
export default defineHook({
|
|
248
243
|
events: {
|
|
249
244
|
async "session.started"(_event, ctx) {
|
|
250
|
-
await postToHoneycomb({ event: "session.started", sessionId: ctx.session.
|
|
245
|
+
await postToHoneycomb({ event: "session.started", sessionId: ctx.session.id });
|
|
251
246
|
},
|
|
252
247
|
async "message.completed"(event) {
|
|
253
248
|
await postToHoneycomb({ event: "message.completed", payload: event.data });
|
|
@@ -282,7 +277,7 @@ in `try`/`catch` — Ash treats a thrown hook as a real failure.
|
|
|
282
277
|
Subagents may carry their own `agent/hooks/` directory. Subagent hooks
|
|
283
278
|
fire only inside the subagent scope; parent-agent hooks do not fire for
|
|
284
279
|
subagent turns, and subagent hooks see only the subagent's own
|
|
285
|
-
|
|
280
|
+
context.
|
|
286
281
|
|
|
287
282
|
## When to use a hook vs a tool vs a provider
|
|
288
283
|
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
"pages": [
|
|
3
3
|
"getting-started",
|
|
4
4
|
"---",
|
|
5
|
-
"project-layout",
|
|
6
5
|
"agent-ts",
|
|
7
|
-
"context-control",
|
|
8
6
|
"skills",
|
|
9
7
|
"tools",
|
|
10
|
-
"hooks",
|
|
11
8
|
"human-in-the-loop",
|
|
12
9
|
"sandbox",
|
|
13
10
|
"channels",
|
|
14
|
-
"auth-and-route-protection",
|
|
15
11
|
"subagents",
|
|
16
12
|
"schedules",
|
|
17
13
|
"connections",
|
|
18
14
|
"---",
|
|
15
|
+
"project-layout",
|
|
16
|
+
"context-control",
|
|
17
|
+
"hooks",
|
|
18
|
+
"auth-and-route-protection",
|
|
19
19
|
"vercel-deployment",
|
|
20
20
|
"runs-and-streaming",
|
|
21
21
|
"session-context",
|
|
@@ -31,7 +31,7 @@ import { defineSandbox } from "experimental-ash/sandbox";
|
|
|
31
31
|
export default defineSandbox({
|
|
32
32
|
async bootstrap({ use }) {
|
|
33
33
|
const sandbox = await use();
|
|
34
|
-
await sandbox.
|
|
34
|
+
await sandbox.run({ command: "apt-get install -y jq" });
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
37
|
```
|
|
@@ -58,7 +58,7 @@ The public lifecycle surface is intentionally small:
|
|
|
58
58
|
sandbox — creation happens at prewarm time and at first-time session-create, both driven by the
|
|
59
59
|
backend factory's options.
|
|
60
60
|
- `sandbox.resolvePath(path)` — translate a logical `/workspace/...` path into the live filesystem
|
|
61
|
-
- `sandbox.
|
|
61
|
+
- `sandbox.run({ command })` — run a shell command inside the sandbox
|
|
62
62
|
|
|
63
63
|
`defineSandbox` lives on `experimental-ash/sandbox`.
|
|
64
64
|
|
|
@@ -114,21 +114,20 @@ and `write_file` tools. The runtime name comes from the file stem (here, `repo_s
|
|
|
114
114
|
## Using The Sandbox From Authored Code
|
|
115
115
|
|
|
116
116
|
Any authored runtime function (tool, step, or model callback) can bind a live sandbox handle via
|
|
117
|
-
`getSandbox()`:
|
|
117
|
+
`ctx.getSandbox()`:
|
|
118
118
|
|
|
119
119
|
```ts
|
|
120
|
-
import { getSandbox } from "experimental-ash/sandbox";
|
|
121
120
|
import { defineTool } from "experimental-ash/tools";
|
|
122
121
|
import { z } from "zod";
|
|
123
122
|
|
|
124
123
|
export default defineTool({
|
|
125
124
|
description: "Append an analytics event to the running session log.",
|
|
126
125
|
inputSchema: z.object({ kind: z.string(), payload: z.string() }),
|
|
127
|
-
async execute({ kind, payload }) {
|
|
128
|
-
const sandbox = await getSandbox();
|
|
129
|
-
await sandbox.
|
|
130
|
-
`echo ${JSON.stringify(JSON.stringify({ kind, payload }))} >> /var/lib/analytics/events.log`,
|
|
131
|
-
);
|
|
126
|
+
async execute({ kind, payload }, ctx) {
|
|
127
|
+
const sandbox = await ctx.getSandbox();
|
|
128
|
+
await sandbox.run({
|
|
129
|
+
command: `echo ${JSON.stringify(JSON.stringify({ kind, payload }))} >> /var/lib/analytics/events.log`,
|
|
130
|
+
});
|
|
132
131
|
return { ok: true };
|
|
133
132
|
},
|
|
134
133
|
});
|
|
@@ -136,7 +135,7 @@ export default defineTool({
|
|
|
136
135
|
|
|
137
136
|
Important rules:
|
|
138
137
|
|
|
139
|
-
- `getSandbox()` takes no arguments and is async.
|
|
138
|
+
- `ctx.getSandbox()` takes no arguments and is async.
|
|
140
139
|
- It only works inside authored runtime execution.
|
|
141
140
|
- Visibility is node-local: the root agent sees the root agent's sandbox; a local subagent sees its
|
|
142
141
|
own sandbox, not the parent's.
|
|
@@ -144,7 +143,7 @@ Important rules:
|
|
|
144
143
|
### Workspace Paths
|
|
145
144
|
|
|
146
145
|
Every backend runs `bash` with `/workspace` as the working directory. `readFile(...)`,
|
|
147
|
-
`writeFile(...)`, and `
|
|
146
|
+
`writeFile(...)`, and `run(...)` all share that single namespace — `/workspace/foo` refers
|
|
148
147
|
to the same file whether the backend is local or Vercel.
|
|
149
148
|
|
|
150
149
|
`sandbox.resolvePath(...)` anchors a sandbox-relative path to `/workspace` and returns the
|
|
@@ -154,15 +153,15 @@ Use it when you need an absolute path to interpolate into a generated command or
|
|
|
154
153
|
hardcoding `/workspace/` yourself:
|
|
155
154
|
|
|
156
155
|
```ts
|
|
157
|
-
const sandbox = await getSandbox();
|
|
156
|
+
const sandbox = await ctx.getSandbox();
|
|
158
157
|
const analysisRoot = sandbox.resolvePath("python-analysis");
|
|
159
158
|
|
|
160
159
|
await sandbox.writeFile("python-analysis/run.py", "print('ok')\n");
|
|
161
|
-
await sandbox.
|
|
160
|
+
await sandbox.run({ command: `python ${JSON.stringify(`${analysisRoot}/run.py`)}` });
|
|
162
161
|
```
|
|
163
162
|
|
|
164
163
|
`readFile(...)` and `writeFile(...)` apply the same anchoring internally, so you only need to
|
|
165
|
-
call `resolvePath(...)` explicitly when building paths for `
|
|
164
|
+
call `resolvePath(...)` explicitly when building paths for `run(...)` or returning them
|
|
166
165
|
from authored helpers.
|
|
167
166
|
|
|
168
167
|
## Subagents Get Their Own Sandbox
|
|
@@ -176,7 +175,7 @@ different skills, so they should not inherit the parent's filesystem.
|
|
|
176
175
|
with that workspace seeded in.
|
|
177
176
|
- If a subagent authors neither, the framework default is used as-is.
|
|
178
177
|
|
|
179
|
-
Inside a subagent's authored code, `getSandbox()` binds to the subagent's own sandbox.
|
|
178
|
+
Inside a subagent's authored code, `ctx.getSandbox()` binds to the subagent's own sandbox.
|
|
180
179
|
|
|
181
180
|
## Lifecycle Semantics
|
|
182
181
|
|
|
@@ -210,18 +209,17 @@ returned. The accepted option shape is whatever the backend's update call accept
|
|
|
210
209
|
factory; `onSession`'s `use()` overrides any overlapping fields post-create.
|
|
211
210
|
|
|
212
211
|
Unlike `bootstrap`, `onSession` runs inside the active Ash runtime context, so user-scoped setup can
|
|
213
|
-
|
|
212
|
+
read `ctx.session` and derive the current principal without baking credentials into the reusable
|
|
214
213
|
template:
|
|
215
214
|
|
|
216
215
|
```ts
|
|
217
|
-
import { getSession } from "experimental-ash/context";
|
|
218
216
|
import { defineSandbox, vercelBackend } from "experimental-ash/sandbox";
|
|
219
217
|
|
|
220
218
|
export default defineSandbox({
|
|
221
219
|
backend: vercelBackend(),
|
|
222
|
-
async onSession({ use }) {
|
|
220
|
+
async onSession({ use, ctx }) {
|
|
223
221
|
const sandbox = await use({ networkPolicy: "deny-all" });
|
|
224
|
-
const user =
|
|
222
|
+
const user = ctx.session.auth.current;
|
|
225
223
|
if (user === null) return;
|
|
226
224
|
|
|
227
225
|
await sandbox.writeFile(
|
|
@@ -236,7 +234,8 @@ export default defineSandbox({
|
|
|
236
234
|
|
|
237
235
|
Ash ships two built-in backends, exposed as factory functions from `experimental-ash/sandbox`:
|
|
238
236
|
|
|
239
|
-
- `vercelBackend(opts?)` — runs the sandbox on Vercel Sandbox
|
|
237
|
+
- `vercelBackend(opts?)` — runs the sandbox on [Vercel Sandbox](https://vercel.com/docs/sandbox) via
|
|
238
|
+
[`@vercel/sandbox`](https://vercel.com/docs/sandbox/sdk-reference).
|
|
240
239
|
- `localBackend(opts?)` — runs the sandbox locally via `just-bash`. Default for `pnpm ash dev`.
|
|
241
240
|
|
|
242
241
|
Attach a backend via `defineSandbox({ backend })`:
|
|
@@ -249,7 +248,7 @@ export default defineSandbox({
|
|
|
249
248
|
backend: vercelBackend({ runtime: "node24", resources: { vcpus: 2 } }),
|
|
250
249
|
async bootstrap({ use }) {
|
|
251
250
|
const sandbox = await use();
|
|
252
|
-
await sandbox.
|
|
251
|
+
await sandbox.run({ command: "git clone https://example.com/repo.git repo" });
|
|
253
252
|
},
|
|
254
253
|
async onSession({ use }) {
|
|
255
254
|
await use({ networkPolicy: "deny-all" });
|
|
@@ -282,9 +281,10 @@ The factory `opts` parameter is forwarded to the underlying SDK's `Sandbox.creat
|
|
|
282
281
|
every fresh sandbox the framework creates — both the template at prewarm time and the session at
|
|
283
282
|
first-time session-create.
|
|
284
283
|
|
|
285
|
-
On resume (when the framework reattaches to an existing
|
|
286
|
-
`Sandbox.
|
|
287
|
-
|
|
284
|
+
On resume (when the framework reattaches to an existing
|
|
285
|
+
[persistent](#session-persistence-and-resume) session via `Sandbox.get`), no `Sandbox.create` call
|
|
286
|
+
happens, so the factory opts are not re-applied. The existing sandbox keeps whatever configuration
|
|
287
|
+
it had from its prior creation.
|
|
288
288
|
|
|
289
289
|
Lifecycle hooks remain update-time and override post-create:
|
|
290
290
|
|
|
@@ -361,9 +361,8 @@ lock down via `onSession`.
|
|
|
361
361
|
|
|
362
362
|
### Timeout
|
|
363
363
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
a different default on the factory, or override per-session in `onSession`:
|
|
364
|
+
Sandbox VMs shut down after a configurable timeout. Ash defaults to **30 minutes**. Set a different
|
|
365
|
+
value on the factory, or override per-session in `onSession`:
|
|
367
366
|
|
|
368
367
|
```ts
|
|
369
368
|
// factory default — applies to every fresh sandbox
|
|
@@ -376,7 +375,28 @@ async onSession({ use }) {
|
|
|
376
375
|
```
|
|
377
376
|
|
|
378
377
|
The maximum timeout depends on your Vercel plan: **5 hours** for Pro/Enterprise, **45 minutes** for
|
|
379
|
-
Hobby.
|
|
378
|
+
Hobby. When the timeout fires, the VM shuts down — but because sessions are
|
|
379
|
+
[persistent](#session-persistence-and-resume), the filesystem state is preserved and the sandbox
|
|
380
|
+
resumes automatically on the next request.
|
|
381
|
+
|
|
382
|
+
### Session Persistence and Resume
|
|
383
|
+
|
|
384
|
+
Sandbox sessions are **persistent by default**: when the VM shuts down (timeout or inactivity), the
|
|
385
|
+
filesystem state is preserved. The next time a message arrives — even days or weeks later — the
|
|
386
|
+
sandbox automatically resumes from that state.
|
|
387
|
+
|
|
388
|
+
This means:
|
|
389
|
+
|
|
390
|
+
- A user can send a message days after the last interaction and pick up where they left off.
|
|
391
|
+
Files created during earlier turns, installed dependencies, and workspace state are all preserved.
|
|
392
|
+
- The resume is transparent to your code — no configuration is required.
|
|
393
|
+
- If a sandbox has been deleted (by cleanup policies or manual deletion), Ash creates a fresh
|
|
394
|
+
session from the prewarmed template snapshot. The user gets a clean sandbox seeded with framework
|
|
395
|
+
defaults, bootstrap output, and workspace files — but any session-specific state from prior turns
|
|
396
|
+
is lost.
|
|
397
|
+
|
|
398
|
+
See the [Vercel Sandbox documentation](https://vercel.com/docs/sandbox) for details on persistence
|
|
399
|
+
behavior and plan-specific retention limits.
|
|
380
400
|
|
|
381
401
|
### Tags
|
|
382
402
|
|
|
@@ -386,8 +406,7 @@ Ash attaches Vercel Sandbox tags for runtime attribution:
|
|
|
386
406
|
- `channel` — the active channel adapter kind
|
|
387
407
|
- `sessionId` — the Ash session id
|
|
388
408
|
|
|
389
|
-
Custom tags can be set on the factory
|
|
390
|
-
`onSession`'s `use()` (applied via `sandbox.update`):
|
|
409
|
+
Custom tags can be set on the factory or via `onSession`'s `use()`:
|
|
391
410
|
|
|
392
411
|
```ts
|
|
393
412
|
backend: vercelBackend({ tags: { team: "infra" } });
|
|
@@ -425,3 +444,5 @@ Important behavior:
|
|
|
425
444
|
- [Tools](./tools.md)
|
|
426
445
|
- [Workspace](./workspace.md)
|
|
427
446
|
- [Session Context](./session-context.md)
|
|
447
|
+
- [Vercel Sandbox](https://vercel.com/docs/sandbox) — platform documentation for Vercel Sandbox
|
|
448
|
+
- [Vercel Sandbox SDK Reference](https://vercel.com/docs/sandbox/sdk-reference) — `@vercel/sandbox` API reference
|