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.
Files changed (119) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/docs/internals/context.md +6 -5
  3. package/dist/docs/internals/core-beliefs.md +2 -2
  4. package/dist/docs/internals/hooks.md +16 -11
  5. package/dist/docs/internals/mechanical-invariants.md +4 -4
  6. package/dist/docs/internals/testing.md +1 -1
  7. package/dist/docs/public/README.md +1 -1
  8. package/dist/docs/public/auth-and-route-protection.md +12 -3
  9. package/dist/docs/public/channels/{README.md → index.md} +2 -2
  10. package/dist/docs/public/channels/slack.md +3 -3
  11. package/dist/docs/public/cli-build-and-debugging.md +1 -1
  12. package/dist/docs/public/faqs.md +4 -5
  13. package/dist/docs/public/hooks.md +18 -23
  14. package/dist/docs/public/meta.json +4 -4
  15. package/dist/docs/public/sandbox.md +51 -30
  16. package/dist/docs/public/schedules.md +1 -1
  17. package/dist/docs/public/session-context.md +26 -28
  18. package/dist/docs/public/skills.md +3 -4
  19. package/dist/docs/public/subagents.md +1 -1
  20. package/dist/docs/public/tools.md +13 -17
  21. package/dist/docs/public/typescript-api.md +10 -11
  22. package/dist/src/channel/session.d.ts +3 -29
  23. package/dist/src/channel/session.js +1 -1
  24. package/dist/src/compiled/.vendor-stamp.json +2 -2
  25. package/dist/src/compiled/@vercel/sandbox/index.d.ts +11 -2
  26. package/dist/src/compiled/@vercel/sandbox/index.js +3 -3
  27. package/dist/src/compiled/@vercel/sandbox/package.json +1 -1
  28. package/dist/src/compiled/_chunks/node/{auth-ZhCJAHxl.js → auth-CVVvWjaK.js} +1 -1
  29. package/dist/src/compiled/_chunks/node/{version-D4IYmfaS.js → version-nR4RSpFw.js} +1 -1
  30. package/dist/src/context/build-callback-context.d.ts +8 -0
  31. package/dist/src/context/build-callback-context.js +1 -0
  32. package/dist/src/context/hook-lifecycle.js +1 -1
  33. package/dist/src/execution/node-step.js +1 -1
  34. package/dist/src/execution/sandbox/bash-tool.d.ts +2 -1
  35. package/dist/src/execution/sandbox/bash-tool.js +1 -1
  36. package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
  37. package/dist/src/execution/sandbox/glob-tool.d.ts +2 -1
  38. package/dist/src/execution/sandbox/glob-tool.js +3 -3
  39. package/dist/src/execution/sandbox/grep-tool.d.ts +2 -1
  40. package/dist/src/execution/sandbox/grep-tool.js +3 -3
  41. package/dist/src/execution/sandbox/read-file-tool.d.ts +2 -1
  42. package/dist/src/execution/sandbox/read-file-tool.js +1 -1
  43. package/dist/src/execution/sandbox/session.js +1 -1
  44. package/dist/src/execution/sandbox/write-file-tool.d.ts +2 -1
  45. package/dist/src/execution/sandbox/write-file-tool.js +1 -1
  46. package/dist/src/execution/tool-compaction.js +1 -1
  47. package/dist/src/harness/code-mode-approval.js +1 -1
  48. package/dist/src/harness/code-mode.js +1 -1
  49. package/dist/src/harness/tool-loop.js +1 -1
  50. package/dist/src/internal/application/package.js +1 -1
  51. package/dist/src/internal/logging.js +1 -1
  52. package/dist/src/internal/workflow-bundle/builder.js +2 -2
  53. 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
  54. 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
  55. 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
  56. package/dist/src/packages/ash-scaffold/src/channels.js +2 -12
  57. package/dist/src/packages/ash-scaffold/src/pnpm-workspace.js +11 -0
  58. package/dist/src/packages/ash-scaffold/src/project.js +1 -1
  59. package/dist/src/packages/ash-scaffold/src/web-template.js +2 -2
  60. package/dist/src/public/channels/auth.d.ts +22 -11
  61. package/dist/src/public/channels/auth.js +1 -1
  62. package/dist/src/public/context/index.d.ts +3 -2
  63. package/dist/src/public/context/index.js +1 -1
  64. package/dist/src/public/definitions/callback-context.d.ts +22 -0
  65. package/dist/src/public/definitions/callback-context.js +1 -0
  66. package/dist/src/public/definitions/hook.d.ts +9 -49
  67. package/dist/src/public/definitions/sandbox.d.ts +1 -1
  68. package/dist/src/public/definitions/tool.d.ts +14 -15
  69. package/dist/src/public/hooks/index.d.ts +1 -1
  70. package/dist/src/public/sandbox/index.d.ts +1 -2
  71. package/dist/src/public/sandbox/index.js +1 -1
  72. package/dist/src/public/sandbox/vercel-sandbox.d.ts +4 -4
  73. package/dist/src/public/skills/index.d.ts +0 -1
  74. package/dist/src/public/skills/index.js +1 -1
  75. package/dist/src/public/tools/defaults.js +1 -1
  76. package/dist/src/public/tools/define-bash-tool.js +1 -1
  77. package/dist/src/public/tools/define-glob-tool.js +1 -1
  78. package/dist/src/public/tools/define-grep-tool.js +1 -1
  79. package/dist/src/public/tools/define-read-file-tool.js +1 -1
  80. package/dist/src/public/tools/define-write-file-tool.js +1 -1
  81. package/dist/src/public/tools/index.d.ts +2 -1
  82. package/dist/src/public/tools/internal.d.ts +4 -0
  83. package/dist/src/public/tools/internal.js +1 -1
  84. package/dist/src/runtime/framework-tools/bash.js +1 -1
  85. package/dist/src/runtime/framework-tools/connection-search.js +1 -1
  86. package/dist/src/runtime/framework-tools/connection-tools.js +1 -1
  87. package/dist/src/runtime/framework-tools/file-state.d.ts +2 -2
  88. package/dist/src/runtime/framework-tools/file-state.js +1 -1
  89. package/dist/src/runtime/framework-tools/glob.js +1 -1
  90. package/dist/src/runtime/framework-tools/grep.js +1 -1
  91. package/dist/src/runtime/framework-tools/read-file.js +2 -2
  92. package/dist/src/runtime/framework-tools/todo.js +1 -1
  93. package/dist/src/runtime/framework-tools/write-file.js +1 -1
  94. package/dist/src/runtime/governance/auth/oidc.js +1 -1
  95. package/dist/src/runtime/governance/auth/token-claims.d.ts +2 -0
  96. package/dist/src/runtime/governance/auth/token-claims.js +1 -1
  97. package/dist/src/runtime/governance/auth/types.d.ts +6 -0
  98. package/dist/src/runtime/types.d.ts +2 -2
  99. package/dist/src/shared/sandbox-session.d.ts +0 -17
  100. package/package.json +3 -3
  101. 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
  102. 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
  103. 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
  104. /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
  105. /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
  106. /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
  107. /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
  108. /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
  109. /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
  110. /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
  111. /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
  112. /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
  113. /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
  114. /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
  115. /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
  116. /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
  117. /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
  118. /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
  119. /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. Public accessors (`getSession()`, `getSandbox()`, `getSkill()`) delegate to it.
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. See
82
- [Hooks](./hooks.md) for the full pipeline and the
83
- `SessionPreparedKey` flag's failure semantics.
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 `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.
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.ash` like every other authored
19
- function. One context, one set of keys, one place to look when debugging.
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.ash` is the same `ContextAccessor` tools, providers, and
22
- channels get. There is no parallel state-patch channel — one
23
- `AshContext`, one set of keys, one place to look when debugging.
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` itself is intentionally tiny: `agent`, `channel`, `session`
26
- identity plus the `ash` accessor. Per-turn `turnId` and `sequence` come
27
- from `LifecycleHookInput.turn` for lifecycle hooks, and from
28
- `event.data.{turnId, sequence}` for stream-event hooks — never duplicated
29
- on the context.
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 `ctx.ash` like every other authored
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.ash` reads hit the subagent's container, not the parent's.
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.ash`.
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.ash` (the unified `ContextAccessor`). The hooks surface
72
- introduces no parallel state-patch channel — lifecycle return shapes
73
- expose only `modelContext` (a one-shot per-call ephemeral message list),
74
- and subagent hooks never read parent ALS context.
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 `getSession()` / `getSandbox()` / `getSkill()` to resolve. Use `run(...)` when only compiled artifacts + bundle cache need scoping.
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 such as `getSession()`, `getSandbox()`, and `getSkill()`
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, `getSession().auth` gives you the caller snapshot.
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"(event, ctx) {
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.initiatorAuth` so the target's event handlers and the
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"(event, ctx) {
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 run inside the workflow context,
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 such as `getSession()` do not work at module top level
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
@@ -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 (sessionId, turn, auth,
60
- parent lineage) via `getSession()` from `experimental-ash/context`. This is
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
- - **Durable context keys + `ContextProvider`** are the supported pattern for
63
- reading or writing session-scoped durable state from authored code — see
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.sessionId });
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`. Runtime context helpers (`getSession`,
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 sessionId: string };
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 { ContextKey } from "experimental-ash/context";
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 GithubProfileKey = new ContextKey<GithubProfile>("myapp.githubProfile");
132
+ const githubProfile = defineState<GithubProfile | null>("myapp.githubProfile", () => null);
138
133
 
139
134
  export default defineHook({
140
135
  lifecycle: {
141
- async session(_input, ctx) {
142
- const profile = await fetchGithubProfile(ctx.session.sessionId);
143
- ctx.ash.set(GithubProfileKey, profile);
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 `ctx.ash.get(GithubProfileKey)`.
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(_input, ctx) {
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.sessionId)}).`,
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.sessionId });
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
- `ctx.ash`.
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.runCommand("apt-get install -y jq");
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.runCommand(command)` — run a shell command inside the 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.runCommand(
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 `runCommand(...)` all share that single namespace — `/workspace/foo` refers
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.runCommand(`python ${JSON.stringify(`${analysisRoot}/run.py`)}`);
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 `runCommand(...)` or returning them
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
- call `getSession()` and derive the current principal without baking credentials into the reusable
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 = getSession().auth.current;
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 via `@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.runCommand("git clone https://example.com/repo.git repo");
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 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.
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
- The `@vercel/sandbox` SDK shuts down idle VMs after a configurable timeout (default 5 minutes). Ash
365
- raises this default to **30 minutes** so the sandbox survives across workflow step boundaries. Set
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. If a sandbox expires between steps, the next step will fail with a `410 Gone` error.
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 (applied at every fresh `Sandbox.create`) or via
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