experimental-ash 0.2.0-alpha.20 → 0.2.0-alpha.21

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/dist/src/channel/slack-channel.d.ts +7 -14
  2. package/dist/src/channel/slack-channel.d.ts.map +1 -1
  3. package/dist/src/channel/slack-channel.js +11 -21
  4. package/dist/src/channel/slack-channel.js.map +1 -1
  5. package/dist/src/channel/types.d.ts +9 -6
  6. package/dist/src/channel/types.d.ts.map +1 -1
  7. package/dist/src/channel/types.js.map +1 -1
  8. package/dist/src/context/accessors.d.ts +13 -12
  9. package/dist/src/context/accessors.d.ts.map +1 -1
  10. package/dist/src/context/accessors.js +16 -17
  11. package/dist/src/context/accessors.js.map +1 -1
  12. package/dist/src/context/container.d.ts +17 -19
  13. package/dist/src/context/container.d.ts.map +1 -1
  14. package/dist/src/context/container.js +21 -14
  15. package/dist/src/context/container.js.map +1 -1
  16. package/dist/src/context/durable-context.d.ts +18 -0
  17. package/dist/src/context/durable-context.d.ts.map +1 -0
  18. package/dist/src/context/durable-context.js +49 -0
  19. package/dist/src/context/durable-context.js.map +1 -0
  20. package/dist/src/context/key.d.ts +34 -82
  21. package/dist/src/context/key.d.ts.map +1 -1
  22. package/dist/src/context/key.js +58 -80
  23. package/dist/src/context/key.js.map +1 -1
  24. package/dist/src/context/keys.d.ts +6 -6
  25. package/dist/src/context/keys.d.ts.map +1 -1
  26. package/dist/src/context/keys.js +8 -6
  27. package/dist/src/context/keys.js.map +1 -1
  28. package/dist/src/context/provider.d.ts +2 -2
  29. package/dist/src/context/provider.d.ts.map +1 -1
  30. package/dist/src/context/run-step.d.ts +6 -8
  31. package/dist/src/context/run-step.d.ts.map +1 -1
  32. package/dist/src/context/run-step.js +13 -17
  33. package/dist/src/context/run-step.js.map +1 -1
  34. package/dist/src/context/seed-keys.d.ts +7 -7
  35. package/dist/src/context/seed-keys.d.ts.map +1 -1
  36. package/dist/src/context/seed-keys.js +19 -7
  37. package/dist/src/context/seed-keys.js.map +1 -1
  38. package/dist/src/context/serialize.d.ts +8 -9
  39. package/dist/src/context/serialize.d.ts.map +1 -1
  40. package/dist/src/context/serialize.js +28 -16
  41. package/dist/src/context/serialize.js.map +1 -1
  42. package/dist/src/execution/continuous-entry.d.ts +6 -8
  43. package/dist/src/execution/continuous-entry.d.ts.map +1 -1
  44. package/dist/src/execution/continuous-entry.js +25 -21
  45. package/dist/src/execution/continuous-entry.js.map +1 -1
  46. package/dist/src/execution/continuous-runtime.d.ts.map +1 -1
  47. package/dist/src/execution/continuous-runtime.js +4 -1
  48. package/dist/src/execution/continuous-runtime.js.map +1 -1
  49. package/dist/src/execution/step-context.d.ts +27 -0
  50. package/dist/src/execution/step-context.d.ts.map +1 -0
  51. package/dist/src/execution/step-context.js +54 -0
  52. package/dist/src/execution/step-context.js.map +1 -0
  53. package/dist/src/execution/tool-compaction.d.ts +1 -3
  54. package/dist/src/execution/tool-compaction.d.ts.map +1 -1
  55. package/dist/src/execution/tool-compaction.js +6 -17
  56. package/dist/src/execution/tool-compaction.js.map +1 -1
  57. package/dist/src/execution/workflow-entry.d.ts +4 -5
  58. package/dist/src/execution/workflow-entry.d.ts.map +1 -1
  59. package/dist/src/execution/workflow-entry.js.map +1 -1
  60. package/dist/src/execution/workflow-runtime.d.ts.map +1 -1
  61. package/dist/src/execution/workflow-runtime.js +3 -2
  62. package/dist/src/execution/workflow-runtime.js.map +1 -1
  63. package/dist/src/execution/workflow-steps.d.ts +2 -2
  64. package/dist/src/execution/workflow-steps.d.ts.map +1 -1
  65. package/dist/src/execution/workflow-steps.js +12 -20
  66. package/dist/src/execution/workflow-steps.js.map +1 -1
  67. package/dist/src/harness/emission.d.ts +1 -1
  68. package/dist/src/harness/emission.js +3 -3
  69. package/dist/src/harness/emission.js.map +1 -1
  70. package/dist/src/harness/input-requests.d.ts +22 -1
  71. package/dist/src/harness/input-requests.d.ts.map +1 -1
  72. package/dist/src/harness/input-requests.js +36 -32
  73. package/dist/src/harness/input-requests.js.map +1 -1
  74. package/dist/src/harness/types.d.ts +15 -3
  75. package/dist/src/harness/types.d.ts.map +1 -1
  76. package/dist/src/harness/types.js +6 -1
  77. package/dist/src/harness/types.js.map +1 -1
  78. package/dist/src/internal/application/package.js +1 -1
  79. package/dist/src/internal/nitro/routes/runtime-stack.d.ts +3 -3
  80. package/dist/src/internal/nitro/routes/runtime-stack.js +3 -3
  81. package/dist/src/public/channels/slack/index.d.ts +5 -0
  82. package/dist/src/public/channels/slack/index.d.ts.map +1 -1
  83. package/dist/src/public/channels/slack/index.js.map +1 -1
  84. package/dist/src/public/definitions/tool.d.ts +3 -4
  85. package/dist/src/public/definitions/tool.d.ts.map +1 -1
  86. package/dist/src/public/definitions/tool.js.map +1 -1
  87. package/dist/src/public/index.d.ts +1 -1
  88. package/dist/src/public/index.d.ts.map +1 -1
  89. package/dist/src/public/index.js +1 -1
  90. package/dist/src/public/index.js.map +1 -1
  91. package/dist/src/public/tools/defaults.d.ts +4 -4
  92. package/dist/src/public/tools/defaults.js +4 -4
  93. package/dist/src/runtime/framework-tools/connection-search.d.ts +2 -2
  94. package/dist/src/runtime/framework-tools/connection-search.d.ts.map +1 -1
  95. package/dist/src/runtime/framework-tools/connection-search.js +2 -2
  96. package/dist/src/runtime/framework-tools/connection-search.js.map +1 -1
  97. package/dist/src/runtime/framework-tools/file-state.d.ts +5 -5
  98. package/dist/src/runtime/framework-tools/file-state.d.ts.map +1 -1
  99. package/dist/src/runtime/framework-tools/file-state.js +6 -8
  100. package/dist/src/runtime/framework-tools/file-state.js.map +1 -1
  101. package/dist/src/runtime/framework-tools/skill.d.ts.map +1 -1
  102. package/dist/src/runtime/framework-tools/skill.js +1 -3
  103. package/dist/src/runtime/framework-tools/skill.js.map +1 -1
  104. package/dist/src/runtime/framework-tools/todo.d.ts.map +1 -1
  105. package/dist/src/runtime/framework-tools/todo.js +2 -4
  106. package/dist/src/runtime/framework-tools/todo.js.map +1 -1
  107. package/dist/src/runtime/sessions/auth.d.ts +1 -1
  108. package/docs/internals/context.md +81 -231
  109. package/docs/public/README.md +19 -17
  110. package/docs/public/channels/README.md +4 -0
  111. package/docs/public/migration-guide.md +71 -0
  112. package/docs/public/session-context.md +46 -17
  113. package/docs/public/tools.md +18 -27
  114. package/docs/public/typescript-api.md +5 -1
  115. package/package.json +1 -1
  116. package/dist/src/context/state.d.ts +0 -27
  117. package/dist/src/context/state.d.ts.map +0 -1
  118. package/dist/src/context/state.js +0 -53
  119. package/dist/src/context/state.js.map +0 -1
@@ -1,276 +1,126 @@
1
1
  # Unified Context
2
2
 
3
- Ash uses a single `AshContext` container, bound by one `AsyncLocalStorage` instance, to carry all
4
- runtime state through the execution stack. There are no secondary `AsyncLocalStorage` bindings, no
5
- custom dehydration protocols, and no out-of-band parameter passing for contextual data.
3
+ Ash uses one `AshContext` container, bound by one `AsyncLocalStorage`, to carry all ambient runtime
4
+ data through execution. There is no second authored-state channel and no separate dehydration path
5
+ for public context.
6
6
 
7
- ## Files To Read
7
+ ## Core Model
8
8
 
9
- - `packages/ash/src/context/key.ts`
10
- - `packages/ash/src/context/container.ts`
11
- - `packages/ash/src/context/provider.ts`
12
- - `packages/ash/src/context/keys.ts`
13
- - `packages/ash/src/context/serialize.ts`
14
- - `packages/ash/src/context/run-step.ts`
15
- - `packages/ash/src/context/accessors.ts`
16
- - `packages/ash/src/context/providers/session.ts`
17
- - `packages/ash/src/context/providers/sandbox.ts`
18
- - `packages/ash/src/context/providers/skill.ts`
19
-
20
- ## Core Primitives
9
+ Ash now has two key categories:
21
10
 
22
- ### ContextKey
11
+ - `ContextKey<T>`: public durable authored context. Values live on `session.context`.
12
+ - `RuntimeContextKey<T>`: private framework-only runtime values. These seed one step and may be
13
+ reconstructed on the next step.
23
14
 
24
- A typed key identifies a value in the container. Keys that hold non-JSON-safe values declare a
25
- codec for serialization at `"use step"` boundaries.
26
-
27
- ```ts
28
- class ContextKey<T> {
29
- readonly name: string;
30
- readonly codec?: {
31
- serialize(value: T): unknown;
32
- deserialize(data: unknown): T | Promise<T>;
33
- };
34
- }
35
- ```
36
-
37
- ### AshContext
38
-
39
- The container interface. One instance per execution scope.
15
+ `AshContext` exposes a uniform API:
40
16
 
41
17
  ```ts
42
18
  interface AshContext {
43
- get<T>(key: ContextKey<T>): T;
44
- tryGet<T>(key: ContextKey<T>): T | undefined;
45
- has(key: ContextKey<unknown>): boolean;
46
- set<T>(key: ContextKey<T>, value: T): void;
19
+ get<T>(key: ContextStoreKey<T>): T;
20
+ tryGet<T>(key: ContextStoreKey<T>): T | undefined;
21
+ has<T>(key: ContextStoreKey<T>): boolean;
22
+ set<T>(key: ContextStoreKey<T>, value: T): void;
23
+ ensure<T>(key: ContextStoreKey<T>, valueOrFactory: T | (() => T)): T;
47
24
  }
48
25
  ```
49
26
 
50
- ### contextStorage
27
+ Public helpers such as `getContext`, `setContext`, `ensureContext`, `getSession`, `getSandbox`, and
28
+ `getSkill` all delegate to this container.
51
29
 
52
- The single `AsyncLocalStorage<AshContext>` instance. Only `runStep` and the runtime entry points
53
- call `contextStorage.run(...)`. Everything else reads via `requireContext()`.
30
+ ## Durable Context Lifecycle
54
31
 
55
- ### requireContext()
32
+ Durable authored context no longer piggybacks on runtime seed serialization.
56
33
 
57
- Returns the active `AshContext` from `contextStorage`, or throws. Authored code (tools, steps,
58
- model callbacks) uses this implicitly through the public accessors.
34
+ Every step now follows this flow:
59
35
 
60
- ## Key Categories
36
+ 1. Deserialize runtime seed keys with `deserializeRuntimeContext(...)`.
37
+ 2. Hydrate all registered public `ContextKey`s from `session.context`.
38
+ 3. Seed internal framework bookkeeping from `session.internal`.
39
+ 4. Apply deliver-time auth and run `channel.onDeliver(...)`.
40
+ 5. Run providers and step code inside `contextStorage.run(...)`.
41
+ 6. Commit the full durable context bag back to `session.context`.
61
42
 
62
- ### Seed keys
43
+ The main files are:
63
44
 
64
- Set by the runtime entry point with live values. Serialized and deserialized at durable step
65
- boundaries via their codec.
45
+ - `packages/ash/src/context/serialize.ts`
46
+ - `packages/ash/src/context/durable-context.ts`
47
+ - `packages/ash/src/execution/step-context.ts`
48
+ - `packages/ash/src/context/run-step.ts`
66
49
 
67
- | Key | Type | Codec |
68
- |-----|------|-------|
69
- | `AuthKey` | `SessionAuthContext \| null` | none (JSON-safe) |
70
- | `InitiatorAuthKey` | `SessionAuthContext \| null` | none |
71
- | `SessionIdKey` | `string` | none |
72
- | `RunIdKey` | `string` | none |
73
- | `ContinuationTokenKey` | `string` | none |
74
- | `ModeKey` | `RunMode` | none |
75
- | `NodeSelectorKey` | `string` | none — points at the active graph node (root or a delegated subagent selector) |
76
- | `ParentSessionKey` | `SessionParent` | none — set only on delegated child contexts to carry parent lineage |
77
- | `ChannelKey` | `Channel` | serializes kind + state, deserializes by hydrating the correct channel class |
78
- | `BundleKey` | `CompiledBundle` | serializes to `compiledArtifactsSource`, deserializes via `getCompiledRuntimeAgentBundle` |
50
+ ## Runtime Seed Serialization
79
51
 
80
- Keys self-register in a global registry at construction time. The serialization
81
- layer uses this registry to resolve string names back to typed keys — there is no
82
- explicit seed key list.
52
+ `serializeRuntimeContext(...)` and `deserializeRuntimeContext(...)` only handle
53
+ `RuntimeContextKey`s marked `serializable: true`.
83
54
 
84
- ### Derived keys
55
+ That includes seed values such as:
85
56
 
86
- Created by providers during `runStep`. Never serialized — providers reconstruct them each step
87
- from seed keys and the harness session.
57
+ - auth and initiator auth
58
+ - session id and continuation token
59
+ - run mode
60
+ - parent lineage
61
+ - compiled bundle
62
+ - serialized channel
88
63
 
89
- | Key | Type | Provider |
90
- |-----|------|----------|
91
- | `SessionKey` | `Session` | `sessionProvider` |
92
- | `SandboxKey` | `SandboxAccess` | `sandboxProvider` |
93
- | `SkillKey` | `SkillAccess` | `skillProvider` |
64
+ Durable authored context is intentionally excluded. `assertNoDurableContextInRuntimeSeed(...)`
65
+ guards against leaking a public `ContextKey` into the runtime seed payload.
94
66
 
95
67
  ## Providers
96
68
 
97
- Providers implement `ContextProvider<T>`:
98
-
99
- ```ts
100
- interface ContextProvider<T> {
101
- readonly key: ContextKey<T>;
102
- create(ctx: AshContext, session: HarnessSession):
103
- ProviderResult<T> | undefined | Promise<ProviderResult<T> | undefined>;
104
- commit?(value: T, session: HarnessSession):
105
- HarnessSession | Promise<HarnessSession>;
106
- }
107
- ```
108
-
109
- `create` receives the context (with seed keys and earlier providers already set) and the current
110
- harness session. It returns a `ProviderResult` with the live value and optionally a modified
111
- session (e.g. skills enriching the system prompt). Returning `undefined` means the provider is not
112
- active for this step.
113
-
114
- `commit` is called after the step completes. It receives the live value (which may have been
115
- mutated during the step) and the harness session from the step result. It returns an updated
116
- session with any state changes persisted. Only providers with mutable state need this (sandbox
117
- snapshots, skill activations).
118
-
119
- Provider ordering matters. The framework providers are baked into `runStep` in dependency order:
120
-
121
- 1. `sessionProvider` — depends only on seed keys
122
- 2. `sandboxProvider` — depends on `BundleKey` and `SessionIdKey`
123
- 3. `skillProvider` — depends on `BundleKey`
124
-
125
- There is no separate provider list to import — `runStep` knows its providers internally.
126
-
127
- ## Step Runner
128
-
129
- `runStep` orchestrates the provider lifecycle around a step callback:
130
-
131
- 1. Iterates providers in order — each may set its key on the context and optionally modify the
132
- harness session.
133
- 2. Runs the callback inside `contextStorage.run(ctx, ...)` so authored code can read the context.
134
- 3. After the callback completes, iterates providers again to call `commit` hooks, persisting
135
- mutable state back onto the session.
136
-
137
- ## Serialization At Step Boundaries
138
-
139
- `serializeContext` and `deserializeContext` handle durable step boundaries generically.
140
- `serializeContext` iterates all entries in the context, calling each codec-backed key's codec
141
- when present. `deserializeContext` iterates the plain JSON record
142
- and resolves each string name back to its registered `ContextKey` via the global key registry.
143
- Keys without a codec are stored as-is (they must be JSON-safe).
144
-
145
- The workflow runtime serializes the context once at `start()` time. Each `"use step"` boundary
146
- deserializes it back, then providers reconstruct derived values.
147
-
148
- The channel codec is owned by the `ChannelKey` definition in `context/keys.ts`.
149
- Channel classes themselves have no knowledge of serialization.
150
-
151
- ## Channel Context
152
-
153
- Auth is separate from the channel — it lives on `RunInput.auth` and `DeliverInput.auth`. There is
154
- no `ContextUpdater` callback, no `withAuth`/`withContext` wrappers, and no deliver-updater-registry
155
- side-channel.
156
-
157
- ### Auth
158
-
159
- Auth lives on the run and deliver inputs, not on the channel itself:
160
-
161
- - `RunInput.auth` — the caller for the current run
162
- - `RunInput.initiatorAuth` — the caller that started the durable session (defaults to `auth` on root
163
- runs; subagent passes the parent's initiator)
164
-
165
- The runtime reads `RunInput.auth` and `RunInput.initiatorAuth` when seeding `AuthKey` and
166
- `InitiatorAuthKey`. On `deliver()`, the runtime updates `AuthKey` from `DeliverInput.auth` so that
167
- `session.auth.current` reflects the follow-up caller while `session.auth.initiator` stays the same.
168
-
169
- ### Custom context via `Channel.onDeliver`
170
-
171
- Channels set custom context keys inside `onDeliver(ctx, payload)`. The method receives a narrow
172
- `ContextAccessor` as its first argument (typed `get`/`set` with `ContextKey`) and returns a
173
- `Promise<StepInput>`. This runs once per turn — both the initial `run()` and each `deliver()` —
174
- after auth keys are seeded. Channel code stays workflow-agnostic because it only touches the
175
- accessor.
176
-
177
- ```ts
178
- class TenantChannel extends Channel {
179
- static readonly kind = "tenant-channel";
180
-
181
- private readonly inner: Channel;
182
- private readonly tenantId: string;
183
-
184
- constructor(state: { readonly tenantId: string }) {
185
- super();
186
- this.tenantId = state.tenantId;
187
- this.inner = new HttpChannel();
188
- }
189
-
190
- async onEvent(event) {
191
- return await this.inner.onEvent(event);
192
- }
193
-
194
- async onDeliver(ctx, payload) {
195
- ctx.set(TenantKey, this.tenantId);
196
- return await this.inner.onDeliver(ctx, payload);
197
- }
198
-
199
- serialize() {
200
- return { tenantId: this.tenantId };
201
- }
202
- }
203
- ```
204
-
205
- ### Deliver path
206
-
207
- On `deliver()`, the channel is deserialized from the session (it was serialized at `run()` time via
208
- the `ChannelKey` codec). The runtime applies `DeliverInput.auth` to update the auth key, then calls
209
- `channel.onDeliver(ctx, payload)` on the deserialized channel. No process-local user registration or
210
- function stashing is needed because the runtime rebuilds the channel from the compiled bundle's
211
- channel registry plus the serialized channel state.
69
+ Providers still reconstruct derived runtime values on each step:
212
70
 
213
- ### Files
71
+ - `SessionKey` via `sessionProvider`
72
+ - `SandboxKey` via `sandboxProvider`
73
+ - `SkillKey` via `skillProvider`
214
74
 
215
- - `packages/ash/src/channel/types.ts` `Channel`, `ContextAccessor` types
216
- - `packages/ash/src/execution/runtime-context.ts` — `buildRunContext` (reads `RunInput.auth`,
217
- seeds auth keys)
218
- - `packages/ash/src/execution/workflow-steps.ts` — deserializes channel, applies deliver auth
219
- - `packages/ash/src/execution/continuous-entry.ts` — applies deliver auth on in-memory channel
75
+ These are runtime-only values, so they use `RuntimeContextKey`, not public `ContextKey`.
220
76
 
221
- ## Integration Points
77
+ Provider-owned mutable state stays separate from authored durable context:
222
78
 
223
- ### Runtime entry (workflow)
79
+ - durable authored values live on `session.context`
80
+ - internal framework bookkeeping lives on `session.internal`
81
+ - provider snapshots such as `sandboxState` stay on their dedicated session fields
224
82
 
225
- `workflow-runtime.ts` builds an `AshContextImpl` via `createRootContext` (for `run()`) or
226
- `createDelegatedChildContext` (for `delegate()`), sets seed keys, serializes, and passes
227
- `serializedContext` to `workflowEntry` via `start()`. The ambient runtime is registered at
228
- this point so durable steps can later recover it via `tryGetAmbientWorkflowRuntime()`.
83
+ ## Channel Setup
229
84
 
230
- ### Runtime entry (continuous)
85
+ `packages/ash/src/execution/step-context.ts` owns the pre-step channel path for both runtimes:
231
86
 
232
- `continuous-runtime.ts` builds an `AshContextImpl` via the same two helpers and passes the
233
- live context directly to `runStep` (no serialization needed since there are no durable step
234
- boundaries). `RuntimeKey` is attached to the context once at run creation.
87
+ 1. rebuild the fresh step context
88
+ 2. hydrate durable context
89
+ 3. seed pending input request bookkeeping
90
+ 4. apply deliver-time auth
91
+ 5. call `channel.onDeliver(ctx, payload)`
92
+ 6. assert that serialized channel state did not change
235
93
 
236
- ### Delegation
94
+ That ordering matters. `onDeliver(...)` runs after durable context hydration, so channel-seeded
95
+ context cannot be overwritten by later hydration.
237
96
 
238
- `runtime.delegate(input)` starts a child run rooted at a non-root graph node. Its
239
- `DelegateInput` extends `RunInput` and adds `parent: SessionParent` and
240
- `target: { selector }`. `createDelegatedChildContext` seeds the child context with the
241
- forwarded initiator auth, the delegate's current auth, the parent lineage, the child node
242
- selector, and the child's own run/session identifiers. The subagent tool wrapper in
243
- `execution/subagent-tool.ts` is the sole in-tree caller of `delegate()`.
97
+ ## Channel State Rule
244
98
 
245
- ### Durable step boundary
99
+ Serialized channel state is immutable after the run starts.
246
100
 
247
- `workflow-steps.ts` deserializes the context from the serialized record, attaches the ambient
248
- workflow runtime via `RuntimeKey` (when one is registered), resolves the active graph node
249
- from `NodeSelectorKey`, and calls `runStep` with the framework providers.
101
+ The runtime snapshots `channel.serialize()` before `onDeliver(...)` and again after step execution.
102
+ If the serialized shape changes, the runtime throws with guidance to move that data into:
250
103
 
251
- ### Tool executors
104
+ - durable context when authored code needs it across turns
105
+ - internal `session.internal` when only framework bookkeeping needs it
252
106
 
253
- Tool executors read from the container via `requireContext()`. Sandbox tools read `SandboxKey`;
254
- the `load_skill` action reads `SkillKey`. Authored tools access context through the public
255
- accessors (`getSession`, `getSandbox`, etc.).
107
+ This keeps channel responsibilities narrow:
256
108
 
257
- ### Public API
109
+ - transport normalization
110
+ - continuation token ownership
111
+ - per-turn context seeding
112
+ - delivery policy
258
113
 
259
- The public accessors in `context/accessors.ts` delegate to `requireContext()`:
114
+ It prevents channels from becoming an untracked second session storage system.
260
115
 
261
- - `getSession()` reads `SessionKey`
262
- - `getSandbox(name)` reads `SandboxKey`
263
- - `getSkill(identifier)` reads `SkillKey`
116
+ ## Runtime Split
264
117
 
265
- ## What Was Removed
118
+ Both runtime flavors now use the same context story:
266
119
 
267
- The unified context replaced these six prior mechanisms:
120
+ - workflow runtime stores runtime seed keys once, then rebuilds a fresh step context on every
121
+ durable step
122
+ - continuous runtime also rebuilds a fresh step context on every step, even though the process is
123
+ still live
268
124
 
269
- - `execution/runtime-context.ts` execution-layer `AsyncLocalStorage` and `RuntimeContext`
270
- - `runtime/session-context.ts` — runtime-layer `AsyncLocalStorage` and `AuthoredRuntimeContext`
271
- - `execution/framework-context.ts` — `FrameworkContext`, `Dehydratable`, channel emitter
272
- dehydration
273
- - `execution/context.ts` — `StepExecutionContext`, `ManagedRuntimeContext`, incremental context
274
- assembly
275
- - The `Dehydratable` interface on channel emitters (replaced by key codecs)
276
- - Separate `session` parameter threading through tool executors and action handlers
125
+ That removes the old semantic split where in-memory runs behaved differently from durable workflow
126
+ runs.
@@ -19,21 +19,22 @@ Read in this order:
19
19
  2. [project-layout.md](./project-layout.md)
20
20
  3. [agent-ts.md](./agent-ts.md)
21
21
  4. [typescript-api.md](./typescript-api.md)
22
- 5. [context-control.md](./context-control.md)
23
- 6. [skills.md](./skills.md)
24
- 7. [tools.md](./tools.md)
25
- 8. [workspace.md](./workspace.md)
26
- 9. [sandboxes.md](./sandboxes.md)
27
- 10. [channels/README.md](./channels/README.md)
28
- 11. [human-in-the-loop.md](./human-in-the-loop.md)
29
- 12. [session-context.md](./session-context.md)
30
- 13. [runs-and-streaming.md](./runs-and-streaming.md)
31
- 14. [subagents.md](./subagents.md)
32
- 15. [schedules.md](./schedules.md)
33
- 16. [evals.md](./evals.md)
34
- 17. [auth-and-route-protection.md](./auth-and-route-protection.md)
35
- 18. [vercel-deployment.md](./vercel-deployment.md)
36
- 19. [cli-build-and-debugging.md](./cli-build-and-debugging.md)
22
+ 5. [migration-guide.md](./migration-guide.md)
23
+ 6. [context-control.md](./context-control.md)
24
+ 7. [skills.md](./skills.md)
25
+ 8. [tools.md](./tools.md)
26
+ 9. [workspace.md](./workspace.md)
27
+ 10. [sandboxes.md](./sandboxes.md)
28
+ 11. [channels/README.md](./channels/README.md)
29
+ 12. [human-in-the-loop.md](./human-in-the-loop.md)
30
+ 13. [session-context.md](./session-context.md)
31
+ 14. [runs-and-streaming.md](./runs-and-streaming.md)
32
+ 15. [subagents.md](./subagents.md)
33
+ 16. [schedules.md](./schedules.md)
34
+ 17. [evals.md](./evals.md)
35
+ 18. [auth-and-route-protection.md](./auth-and-route-protection.md)
36
+ 19. [vercel-deployment.md](./vercel-deployment.md)
37
+ 20. [cli-build-and-debugging.md](./cli-build-and-debugging.md)
37
38
 
38
39
  ## The Public Mental Model
39
40
 
@@ -56,10 +57,11 @@ Ash then gives you:
56
57
  - a stable HTTP message route
57
58
  - optional channel webhook routes
58
59
  - a reconnectable session stream
59
- - durable session state across turns
60
+ - durable session context across turns
60
61
  - a shared runtime workspace
61
62
  - optional isolated sandboxes
62
- - typed runtime helpers such as `getSession()`, `getSandbox()`, and `getSkill()`
63
+ - typed runtime helpers such as `getSession()`, `getContext()`, `setContext()`, `ensureContext()`,
64
+ `getSandbox()`, and `getSkill()`
63
65
 
64
66
  ## The Runtime Shape
65
67
 
@@ -9,6 +9,7 @@ Channels are the transport layer in Ash's channel-harness-runtime split. A chann
9
9
  - deriving or resuming the stable `continuationToken`
10
10
  - applying route auth and network policy
11
11
  - deciding how runtime events are delivered back to the platform
12
+ - seeding per-turn durable context inside `onDeliver(...)`
12
13
 
13
14
  The runtime and harness still own the model turn, tool execution, compaction, and session
14
15
  persistence.
@@ -86,6 +87,9 @@ Use a custom channel when you want:
86
87
  - transport-specific delivery behavior
87
88
  - custom request parsing before the runtime turn starts
88
89
 
90
+ Channel constructor state is for stable serialized transport identity only. Once a run starts, keep
91
+ that state immutable and move mutable per-session data into durable context or `session.internal`.
92
+
89
93
  ## What To Read Next
90
94
 
91
95
  - [`../project-layout.md`](../project-layout.md)
@@ -0,0 +1,71 @@
1
+ # Migration Guide
2
+
3
+ Ash now has one durable authored-context model.
4
+
5
+ ## Breaking Changes
6
+
7
+ - `getState(key)` is now `getContext(key)`.
8
+ - `setState(key, value)` is now `setContext(key, value)`.
9
+ - `ensureContext(key, valueOrFactory)` replaces `ContextKey({ initial })`.
10
+ - `ContextKeyOptions.initial` was removed.
11
+ - Public `ContextKey` values are session-durable by default.
12
+ - `HarnessSession.state` is now split into `session.context` and internal `session.internal`.
13
+ - Tool compaction hooks no longer return `sessionPatch`. Mutate durable context through `ctx` instead.
14
+
15
+ ## Before
16
+
17
+ ```ts
18
+ import { ContextKey, getState, setState } from "experimental-ash";
19
+
20
+ const NotesKey = new ContextKey("myapp.notes", {
21
+ initial: () => ({ notes: [] }),
22
+ });
23
+
24
+ setState(NotesKey, (current) => ({
25
+ notes: [...current.notes, "hello"],
26
+ }));
27
+
28
+ return getState(NotesKey);
29
+ ```
30
+
31
+ ## After
32
+
33
+ ```ts
34
+ import { ContextKey, ensureContext, getContext, setContext } from "experimental-ash";
35
+
36
+ const NotesKey = new ContextKey("myapp.notes");
37
+
38
+ const current = ensureContext(NotesKey, () => ({ notes: [] }));
39
+
40
+ setContext(NotesKey, {
41
+ notes: [...current.notes, "hello"],
42
+ });
43
+
44
+ return getContext(NotesKey);
45
+ ```
46
+
47
+ ## Channel Guidance
48
+
49
+ - Use `channel.onDeliver(ctx, payload)` to seed or extend durable context on every turn.
50
+ - Treat serialized channel state as immutable after the run starts.
51
+ - Move mutable per-session data into durable context or `session.internal`, not onto the channel instance.
52
+
53
+ ## Compaction Hooks
54
+
55
+ Before:
56
+
57
+ ```ts
58
+ onCompact() {
59
+ return {
60
+ sessionPatch: { state: {} },
61
+ };
62
+ }
63
+ ```
64
+
65
+ After:
66
+
67
+ ```ts
68
+ onCompact({ ctx }) {
69
+ ctx.set(NotesKey, { notes: [] });
70
+ }
71
+ ```
@@ -1,8 +1,11 @@
1
1
  # Session Context
2
2
 
3
- Ash exposes three runtime helpers for authored code:
3
+ Ash exposes six runtime helpers for authored code:
4
4
 
5
5
  - `getSession()`
6
+ - `getContext(key)`
7
+ - `setContext(key, value)`
8
+ - `ensureContext(key, valueOrFactory)`
6
9
  - `getSandbox(name)`
7
10
  - `getSkill(identifier)`
8
11
 
@@ -93,6 +96,27 @@ Important behavior:
93
96
 
94
97
  See [`skills.md`](./skills.md) for the full authoring model.
95
98
 
99
+ ## Durable Authored Context
100
+
101
+ `ContextKey` values are session-durable by default. Once authored code sets a key, Ash hydrates it
102
+ from `session.context` before later steps and commits it back after each step.
103
+
104
+ ```ts
105
+ import { ContextKey, ensureContext, getContext, setContext } from "experimental-ash";
106
+
107
+ const TenantKey = new ContextKey<string>("myapp.tenant");
108
+ const NotesKey = new ContextKey<{ readonly notes: readonly string[] }>("myapp.notes");
109
+
110
+ const tenant = getContext(TenantKey);
111
+ const notes = ensureContext(NotesKey, () => ({ notes: [] }));
112
+
113
+ setContext(NotesKey, {
114
+ notes: [...notes.notes, `Handled tenant ${tenant}`],
115
+ });
116
+ ```
117
+
118
+ Use `ensureContext` when a key needs a default value. `ContextKey({ initial })` no longer exists.
119
+
96
120
  ## Passing Custom Context From a Channel
97
121
 
98
122
  Channels can inject custom typed context into the agent inside `onDeliver`. This is useful when a
@@ -115,8 +139,9 @@ export const TenantKey = new ContextKey<string>("myapp.tenant");
115
139
  ### Setting context from a channel
116
140
 
117
141
  Wrap an existing channel with `onDeliver` to set custom context keys. The hook receives a narrow
118
- `ContextAccessor` (typed `get`/`set`) as its first argument and a `DeliverPayload` as the second. It
119
- runs on both `run()` and `deliver()`.
142
+ `ContextAccessor` (`get`, `tryGet`, `has`, `set`, `ensure`) as its first argument and a
143
+ `DeliverPayload` as the second. It runs on both `run()` and `deliver()`, after Ash has hydrated the
144
+ durable context bag for that turn.
120
145
 
121
146
  `agent/channels/slack.ts`
122
147
 
@@ -143,7 +168,7 @@ class TenantSlackChannel extends SlackChannel<TenantSlackState> {
143
168
  }
144
169
 
145
170
  async onDeliver(ctx, payload) {
146
- ctx.set(TenantKey, this.state.tenantId);
171
+ ctx.set(TenantKey, this.state().tenantId);
147
172
  return await super.onDeliver(ctx, payload);
148
173
  }
149
174
  }
@@ -156,17 +181,20 @@ export default slackRoute({
156
181
  Auth lives on `RunInput.auth` and `DeliverInput.auth`, not on the channel object. There is no
157
182
  `withAuth` or `withContext` wrapper.
158
183
 
184
+ Keep channel constructor state immutable after the run starts. Stable transport identity belongs in
185
+ serialized channel state; mutable per-session data belongs in durable context or `session.internal`.
186
+
159
187
  ### Reading context from a tool
160
188
 
161
189
  ```ts
162
- import { getState } from "experimental-ash";
190
+ import { getContext } from "experimental-ash";
163
191
  import { defineTool } from "experimental-ash/tools";
164
192
  import { TenantKey } from "../channels/keys.js";
165
193
 
166
194
  export default defineTool({
167
195
  description: "Return the active tenant.",
168
196
  async execute() {
169
- const tenant = getState(TenantKey);
197
+ const tenant = getContext(TenantKey);
170
198
  return { tenant };
171
199
  },
172
200
  });
@@ -175,8 +203,8 @@ export default defineTool({
175
203
  ### Context on deliver
176
204
 
177
205
  `onDeliver` runs on every turn, including `deliver()` follow-ups. The channel is deserialized from
178
- the session, so its state (including any custom fields like `tenantId`) is available on follow-up
179
- turns.
206
+ the session, so its stable serialized state (including custom fields like `tenantId`) is available
207
+ on follow-up turns.
180
208
 
181
209
  Auth on `deliver()` is also honored — `session.auth.current` reflects the caller of each follow-up
182
210
  message, not just the session initiator. Deliver-time auth is passed via `DeliverInput.auth` and the
@@ -201,17 +229,18 @@ explaining the required scope.
201
229
 
202
230
  ## How It Works
203
231
 
204
- All three accessors read from the same `AshContext` container bound by a single `AsyncLocalStorage`.
205
- The framework sets up this context before invoking authored code:
232
+ All runtime helpers read from the same `AshContext` container bound by a single
233
+ `AsyncLocalStorage`. The framework sets up this context before invoking authored code:
206
234
 
207
- 1. The runtime entry point creates an `AshContext` and populates seed keys (auth, session ID,
208
- channel, compiled bundle).
209
- 2. Before each step, providers create derived values (session metadata, sandbox access, skill
210
- access) from the seed keys and the harness session.
211
- 3. The step callback runs inside the `AsyncLocalStorage` scope, making the context available to
235
+ 1. The runtime entry point creates an `AshContext` and serializes only runtime seed keys such as
236
+ auth, session id, channel, and compiled bundle.
237
+ 2. Before each step, Ash rebuilds a fresh context, hydrates durable authored context from
238
+ `session.context`, seeds runtime-only bookkeeping, and runs `channel.onDeliver(...)`.
239
+ 3. Providers create derived values such as session metadata, sandbox access, and skill access.
240
+ 4. The step callback runs inside the `AsyncLocalStorage` scope, making the context available to
212
241
  all authored code in the call chain.
213
- 4. After the step, providers with mutable state (sandboxes, skills) commit their changes back onto
214
- the session for persistence.
242
+ 5. After the step, Ash commits the full durable context bag back to `session.context` and lets
243
+ mutable providers persist any provider-owned state.
215
244
 
216
245
  This lifecycle is fully managed by the framework. Authored code only needs to call the public
217
246
  accessors.