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

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 (185) hide show
  1. package/dist/src/channel/slack-channel.d.ts +14 -7
  2. package/dist/src/channel/slack-channel.d.ts.map +1 -1
  3. package/dist/src/channel/slack-channel.js +21 -11
  4. package/dist/src/channel/slack-channel.js.map +1 -1
  5. package/dist/src/channel/types.d.ts +14 -14
  6. package/dist/src/channel/types.d.ts.map +1 -1
  7. package/dist/src/channel/types.js +6 -0
  8. package/dist/src/channel/types.js.map +1 -1
  9. package/dist/src/context/accessors.d.ts +24 -10
  10. package/dist/src/context/accessors.d.ts.map +1 -1
  11. package/dist/src/context/accessors.js +34 -16
  12. package/dist/src/context/accessors.js.map +1 -1
  13. package/dist/src/context/container.d.ts +43 -20
  14. package/dist/src/context/container.d.ts.map +1 -1
  15. package/dist/src/context/container.js +54 -22
  16. package/dist/src/context/container.js.map +1 -1
  17. package/dist/src/context/key.d.ts +68 -39
  18. package/dist/src/context/key.d.ts.map +1 -1
  19. package/dist/src/context/key.js +49 -62
  20. package/dist/src/context/key.js.map +1 -1
  21. package/dist/src/context/keys.d.ts +6 -6
  22. package/dist/src/context/keys.d.ts.map +1 -1
  23. package/dist/src/context/keys.js +7 -9
  24. package/dist/src/context/keys.js.map +1 -1
  25. package/dist/src/context/node.d.ts +2 -2
  26. package/dist/src/context/node.d.ts.map +1 -1
  27. package/dist/src/context/node.js +1 -1
  28. package/dist/src/context/node.js.map +1 -1
  29. package/dist/src/context/provider.d.ts +24 -11
  30. package/dist/src/context/provider.d.ts.map +1 -1
  31. package/dist/src/context/providers/connection.d.ts +2 -2
  32. package/dist/src/context/providers/connection.d.ts.map +1 -1
  33. package/dist/src/context/providers/connection.js +1 -1
  34. package/dist/src/context/providers/connection.js.map +1 -1
  35. package/dist/src/context/providers/sandbox.d.ts +2 -2
  36. package/dist/src/context/providers/sandbox.d.ts.map +1 -1
  37. package/dist/src/context/providers/sandbox.js +2 -2
  38. package/dist/src/context/providers/sandbox.js.map +1 -1
  39. package/dist/src/context/providers/session.d.ts +2 -2
  40. package/dist/src/context/providers/session.d.ts.map +1 -1
  41. package/dist/src/context/providers/session.js +4 -4
  42. package/dist/src/context/providers/session.js.map +1 -1
  43. package/dist/src/context/providers/skill.d.ts +2 -2
  44. package/dist/src/context/providers/skill.d.ts.map +1 -1
  45. package/dist/src/context/providers/skill.js +2 -2
  46. package/dist/src/context/providers/skill.js.map +1 -1
  47. package/dist/src/context/run-step.d.ts +9 -8
  48. package/dist/src/context/run-step.d.ts.map +1 -1
  49. package/dist/src/context/run-step.js +32 -22
  50. package/dist/src/context/run-step.js.map +1 -1
  51. package/dist/src/context/seed-keys.d.ts +7 -7
  52. package/dist/src/context/seed-keys.d.ts.map +1 -1
  53. package/dist/src/context/seed-keys.js +7 -19
  54. package/dist/src/context/seed-keys.js.map +1 -1
  55. package/dist/src/context/serialize.d.ts +10 -9
  56. package/dist/src/context/serialize.d.ts.map +1 -1
  57. package/dist/src/context/serialize.js +18 -30
  58. package/dist/src/context/serialize.js.map +1 -1
  59. package/dist/src/execution/continuous-entry.d.ts +8 -6
  60. package/dist/src/execution/continuous-entry.d.ts.map +1 -1
  61. package/dist/src/execution/continuous-entry.js +27 -25
  62. package/dist/src/execution/continuous-entry.js.map +1 -1
  63. package/dist/src/execution/continuous-runtime.d.ts.map +1 -1
  64. package/dist/src/execution/continuous-runtime.js +1 -4
  65. package/dist/src/execution/continuous-runtime.js.map +1 -1
  66. package/dist/src/execution/node-step.js +1 -0
  67. package/dist/src/execution/node-step.js.map +1 -1
  68. package/dist/src/execution/runtime-context.d.ts +3 -3
  69. package/dist/src/execution/runtime-context.d.ts.map +1 -1
  70. package/dist/src/execution/runtime-context.js +3 -3
  71. package/dist/src/execution/runtime-context.js.map +1 -1
  72. package/dist/src/execution/sandboxes/read-file-tool.js +2 -2
  73. package/dist/src/execution/sandboxes/read-file-tool.js.map +1 -1
  74. package/dist/src/execution/sandboxes/require-sandbox.js +2 -2
  75. package/dist/src/execution/sandboxes/require-sandbox.js.map +1 -1
  76. package/dist/src/execution/sandboxes/write-file-tool.d.ts.map +1 -1
  77. package/dist/src/execution/sandboxes/write-file-tool.js +3 -3
  78. package/dist/src/execution/sandboxes/write-file-tool.js.map +1 -1
  79. package/dist/src/execution/subagent-tool.js +6 -6
  80. package/dist/src/execution/subagent-tool.js.map +1 -1
  81. package/dist/src/execution/tool-compaction.d.ts +3 -1
  82. package/dist/src/execution/tool-compaction.d.ts.map +1 -1
  83. package/dist/src/execution/tool-compaction.js +19 -8
  84. package/dist/src/execution/tool-compaction.js.map +1 -1
  85. package/dist/src/execution/types.d.ts +1 -0
  86. package/dist/src/execution/types.d.ts.map +1 -1
  87. package/dist/src/execution/workflow-entry.d.ts +5 -4
  88. package/dist/src/execution/workflow-entry.d.ts.map +1 -1
  89. package/dist/src/execution/workflow-entry.js +8 -3
  90. package/dist/src/execution/workflow-entry.js.map +1 -1
  91. package/dist/src/execution/workflow-runtime.d.ts.map +1 -1
  92. package/dist/src/execution/workflow-runtime.js +2 -3
  93. package/dist/src/execution/workflow-runtime.js.map +1 -1
  94. package/dist/src/execution/workflow-steps.d.ts +2 -2
  95. package/dist/src/execution/workflow-steps.d.ts.map +1 -1
  96. package/dist/src/execution/workflow-steps.js +42 -17
  97. package/dist/src/execution/workflow-steps.js.map +1 -1
  98. package/dist/src/harness/emission.d.ts +1 -1
  99. package/dist/src/harness/emission.js +3 -3
  100. package/dist/src/harness/emission.js.map +1 -1
  101. package/dist/src/harness/execute-tool.d.ts +1 -0
  102. package/dist/src/harness/execute-tool.d.ts.map +1 -1
  103. package/dist/src/harness/execute-tool.js.map +1 -1
  104. package/dist/src/harness/input-requests.d.ts +2 -22
  105. package/dist/src/harness/input-requests.d.ts.map +1 -1
  106. package/dist/src/harness/input-requests.js +47 -41
  107. package/dist/src/harness/input-requests.js.map +1 -1
  108. package/dist/src/harness/tool-loop.d.ts.map +1 -1
  109. package/dist/src/harness/tool-loop.js +19 -1
  110. package/dist/src/harness/tool-loop.js.map +1 -1
  111. package/dist/src/harness/tools.js +5 -1
  112. package/dist/src/harness/tools.js.map +1 -1
  113. package/dist/src/harness/types.d.ts +3 -15
  114. package/dist/src/harness/types.d.ts.map +1 -1
  115. package/dist/src/harness/types.js +1 -6
  116. package/dist/src/harness/types.js.map +1 -1
  117. package/dist/src/internal/application/package.js +1 -1
  118. package/dist/src/internal/authored-definition/connection.d.ts.map +1 -1
  119. package/dist/src/internal/authored-definition/connection.js +14 -1
  120. package/dist/src/internal/authored-definition/connection.js.map +1 -1
  121. package/dist/src/internal/nitro/routes/runtime-stack.d.ts +3 -3
  122. package/dist/src/internal/nitro/routes/runtime-stack.js +3 -3
  123. package/dist/src/public/channels/slack/index.d.ts +0 -5
  124. package/dist/src/public/channels/slack/index.d.ts.map +1 -1
  125. package/dist/src/public/channels/slack/index.js.map +1 -1
  126. package/dist/src/public/definitions/connections/mcp.d.ts +13 -0
  127. package/dist/src/public/definitions/connections/mcp.d.ts.map +1 -1
  128. package/dist/src/public/definitions/connections/mcp.js.map +1 -1
  129. package/dist/src/public/definitions/tool.d.ts +10 -6
  130. package/dist/src/public/definitions/tool.d.ts.map +1 -1
  131. package/dist/src/public/definitions/tool.js.map +1 -1
  132. package/dist/src/public/index.d.ts +2 -1
  133. package/dist/src/public/index.d.ts.map +1 -1
  134. package/dist/src/public/index.js +1 -1
  135. package/dist/src/public/index.js.map +1 -1
  136. package/dist/src/public/tools/defaults.d.ts +4 -4
  137. package/dist/src/public/tools/defaults.js +4 -4
  138. package/dist/src/runtime/connections/registry.d.ts +6 -0
  139. package/dist/src/runtime/connections/registry.d.ts.map +1 -1
  140. package/dist/src/runtime/connections/registry.js +8 -0
  141. package/dist/src/runtime/connections/registry.js.map +1 -1
  142. package/dist/src/runtime/connections/types.d.ts +2 -0
  143. package/dist/src/runtime/connections/types.d.ts.map +1 -1
  144. package/dist/src/runtime/framework-tools/connection-execute.d.ts.map +1 -1
  145. package/dist/src/runtime/framework-tools/connection-execute.js +17 -6
  146. package/dist/src/runtime/framework-tools/connection-execute.js.map +1 -1
  147. package/dist/src/runtime/framework-tools/connection-search.d.ts +2 -2
  148. package/dist/src/runtime/framework-tools/connection-search.d.ts.map +1 -1
  149. package/dist/src/runtime/framework-tools/connection-search.js +4 -4
  150. package/dist/src/runtime/framework-tools/connection-search.js.map +1 -1
  151. package/dist/src/runtime/framework-tools/file-state.d.ts +5 -7
  152. package/dist/src/runtime/framework-tools/file-state.d.ts.map +1 -1
  153. package/dist/src/runtime/framework-tools/file-state.js +4 -6
  154. package/dist/src/runtime/framework-tools/file-state.js.map +1 -1
  155. package/dist/src/runtime/framework-tools/skill.js +3 -3
  156. package/dist/src/runtime/framework-tools/skill.js.map +1 -1
  157. package/dist/src/runtime/framework-tools/todo.js +3 -3
  158. package/dist/src/runtime/framework-tools/todo.js.map +1 -1
  159. package/dist/src/runtime/prompt/connections.d.ts.map +1 -1
  160. package/dist/src/runtime/prompt/connections.js +3 -2
  161. package/dist/src/runtime/prompt/connections.js.map +1 -1
  162. package/dist/src/runtime/resolve-agent-graph.js +3 -2
  163. package/dist/src/runtime/resolve-agent-graph.js.map +1 -1
  164. package/dist/src/runtime/resolve-connection.d.ts.map +1 -1
  165. package/dist/src/runtime/resolve-connection.js +3 -0
  166. package/dist/src/runtime/resolve-connection.js.map +1 -1
  167. package/dist/src/runtime/sessions/auth.d.ts +1 -1
  168. package/dist/src/runtime/types.d.ts +13 -0
  169. package/dist/src/runtime/types.d.ts.map +1 -1
  170. package/docs/internals/context.md +257 -81
  171. package/docs/public/README.md +17 -19
  172. package/docs/public/channels/README.md +9 -3
  173. package/docs/public/session-context.md +95 -49
  174. package/docs/public/tools.md +25 -17
  175. package/docs/public/typescript-api.md +2 -6
  176. package/package.json +1 -1
  177. package/dist/src/context/durable-context.d.ts +0 -18
  178. package/dist/src/context/durable-context.d.ts.map +0 -1
  179. package/dist/src/context/durable-context.js +0 -49
  180. package/dist/src/context/durable-context.js.map +0 -1
  181. package/dist/src/execution/step-context.d.ts +0 -27
  182. package/dist/src/execution/step-context.d.ts.map +0 -1
  183. package/dist/src/execution/step-context.js +0 -54
  184. package/dist/src/execution/step-context.js.map +0 -1
  185. package/docs/public/migration-guide.md +0 -71
@@ -1,13 +1,15 @@
1
1
  # Session Context
2
2
 
3
- Ash exposes six runtime helpers for authored code:
3
+ Ash exposes runtime helpers for authored code:
4
4
 
5
5
  - `getSession()`
6
- - `getContext(key)`
7
- - `setContext(key, value)`
8
- - `ensureContext(key, valueOrFactory)`
9
6
  - `getSandbox(name)`
10
7
  - `getSkill(identifier)`
8
+ - `getContext(key)` — returns `T | undefined`
9
+ - `requireContext(key)` — returns `T`, throws if unset
10
+ - `hasContext(key)` — returns `boolean`
11
+ - `setContext(key, valueOrUpdater)` — returns `T`
12
+ - `ensureContext(key, factory)` — returns `T`
11
13
 
12
14
  These APIs work only inside active authored runtime execution such as tools and other Ash-invoked
13
15
  callbacks. Under the hood, they read from a single `AshContext` container that the framework binds
@@ -96,32 +98,11 @@ Important behavior:
96
98
 
97
99
  See [`skills.md`](./skills.md) for the full authoring model.
98
100
 
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
-
120
101
  ## Passing Custom Context From a Channel
121
102
 
122
- Channels can inject custom typed context into the agent inside `onDeliver`. This is useful when a
123
- channel needs to pass platform-specific metadata (tenant ID, feature flags, etc.) that authored
124
- tools can read.
103
+ Channels can inject custom typed durable context into the agent inside `onDeliver`. This is useful
104
+ when a channel needs to pass platform-specific metadata (tenant ID, feature flags, etc.) that
105
+ authored tools can read on the same turn and on later turns.
125
106
 
126
107
  ### Defining a key
127
108
 
@@ -139,9 +120,8 @@ export const TenantKey = new ContextKey<string>("myapp.tenant");
139
120
  ### Setting context from a channel
140
121
 
141
122
  Wrap an existing channel with `onDeliver` to set custom context keys. The hook receives a narrow
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.
123
+ `ContextAccessor` (typed `get`/`set`) as its first argument and a `DeliverPayload` as the second. It
124
+ runs on both `run()` and `deliver()`.
145
125
 
146
126
  `agent/channels/slack.ts`
147
127
 
@@ -167,8 +147,8 @@ class TenantSlackChannel extends SlackChannel<TenantSlackState> {
167
147
  });
168
148
  }
169
149
 
170
- async onDeliver(ctx, payload) {
171
- ctx.set(TenantKey, this.state().tenantId);
150
+ override async onDeliver(ctx, payload) {
151
+ ctx.set(TenantKey, this.state.tenantId);
172
152
  return await super.onDeliver(ctx, payload);
173
153
  }
174
154
  }
@@ -181,30 +161,96 @@ export default slackRoute({
181
161
  Auth lives on `RunInput.auth` and `DeliverInput.auth`, not on the channel object. There is no
182
162
  `withAuth` or `withContext` wrapper.
183
163
 
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
-
187
164
  ### Reading context from a tool
188
165
 
189
166
  ```ts
190
- import { getContext } from "experimental-ash";
167
+ import { requireContext } from "experimental-ash";
191
168
  import { defineTool } from "experimental-ash/tools";
192
169
  import { TenantKey } from "../channels/keys.js";
193
170
 
194
171
  export default defineTool({
195
172
  description: "Return the active tenant.",
196
173
  async execute() {
197
- const tenant = getContext(TenantKey);
174
+ const tenant = requireContext(TenantKey);
198
175
  return { tenant };
199
176
  },
200
177
  });
201
178
  ```
202
179
 
180
+ ### Rebuilding virtual values with `ContextProvider`
181
+
182
+ If a channel needs to vend a live non-serializable object each step, declare a static
183
+ `contextProviders` array on the channel class. This must be a class field such as
184
+ `static readonly contextProviders = [...]`; the runtime reads providers from the channel class, not
185
+ from the channel instance.
186
+
187
+ For Slack subclasses, the intended pattern is:
188
+
189
+ - `onDeliver()` writes durable serializable facts such as tenant IDs, channel IDs, or installation
190
+ IDs.
191
+ - `static readonly contextProviders = [...]` derives live step-local values such as SDK clients
192
+ from those durable keys.
193
+ - tools and other authored step code read the derived value with `requireContext(...)` or
194
+ `getContext(...)`.
195
+
196
+ Providers run after `onDeliver()` and rebuild virtual values for the current step only, so
197
+ `onDeliver()` should not expect provider output to exist yet.
198
+
199
+ ```ts
200
+ import {
201
+ ContextKey,
202
+ type ContextProvider,
203
+ } from "experimental-ash";
204
+ import {
205
+ slackRoute,
206
+ SlackChannel,
207
+ type SlackChannelState,
208
+ } from "experimental-ash/channels/slack";
209
+
210
+ const TenantKey = new ContextKey<string>("myapp.tenant");
211
+ const SlackClientKey = new ContextKey<{ readonly tenantId: string }>("myapp.slackClient");
212
+
213
+ const slackClientProvider: ContextProvider<{ readonly tenantId: string }> = {
214
+ key: SlackClientKey,
215
+ create(ctx) {
216
+ return { tenantId: ctx.require(TenantKey) };
217
+ },
218
+ };
219
+
220
+ interface TenantSlackState extends SlackChannelState {
221
+ readonly tenantId: string;
222
+ }
223
+
224
+ class TenantSlackChannel extends SlackChannel<TenantSlackState> {
225
+ static override readonly kind = "tenant-slack";
226
+ static readonly contextProviders = [slackClientProvider];
227
+
228
+ constructor(state: SlackChannelState) {
229
+ super({
230
+ ...state,
231
+ tenantId: lookupTenant(state),
232
+ });
233
+ }
234
+
235
+ override async onDeliver(ctx, payload) {
236
+ ctx.set(TenantKey, this.state.tenantId);
237
+ return await super.onDeliver(ctx, payload);
238
+ }
239
+ }
240
+
241
+ export default slackRoute({
242
+ channel: TenantSlackChannel,
243
+ });
244
+ ```
245
+
246
+ Provider output is readable through `requireContext(SlackClientKey)` or
247
+ `getContext(SlackClientKey)` during that step, but it is not serialized.
248
+
203
249
  ### Context on deliver
204
250
 
205
251
  `onDeliver` runs on every turn, including `deliver()` follow-ups. The channel is deserialized from
206
- the session, so its stable serialized state (including custom fields like `tenantId`) is available
207
- on follow-up turns.
252
+ the session, so its state (including any custom fields like `tenantId`) is available on follow-up
253
+ turns.
208
254
 
209
255
  Auth on `deliver()` is also honored — `session.auth.current` reflects the caller of each follow-up
210
256
  message, not just the session initiator. Deliver-time auth is passed via `DeliverInput.auth` and the
@@ -229,18 +275,18 @@ explaining the required scope.
229
275
 
230
276
  ## How It Works
231
277
 
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:
278
+ All three accessors read from the same `AshContext` container bound by a single `AsyncLocalStorage`.
279
+ The framework sets up this context before invoking authored code:
234
280
 
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.
281
+ 1. The runtime entry point creates an `AshContext` and populates durable seed keys (auth, session
282
+ ID, channel, compiled bundle).
283
+ 2. `onDeliver()` can write durable authored context for the turn.
284
+ 3. Before each step, providers create virtual step-local values (session metadata, sandbox access,
285
+ skill access, channel-scoped live objects) from the current context.
240
286
  4. The step callback runs inside the `AsyncLocalStorage` scope, making the context available to
241
287
  all authored code in the call chain.
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.
288
+ 5. After the step, framework providers with provider-owned mutable state (for example sandboxes)
289
+ commit those changes back onto the harness session.
244
290
 
245
291
  This lifecycle is fully managed by the framework. Authored code only needs to call the public
246
292
  accessors.
@@ -36,8 +36,8 @@ exposed to the model as `get_weather`; a file at `agent/tools/get-weather.ts` is
36
36
  `get-weather`. You can override that by setting `name` in the tool definition.
37
37
 
38
38
  `defineTool`, `disableTool`, `defineBashTool`, `defineReadFileTool`, and `defineWriteFileTool` live on the `experimental-ash/tools` subpath.
39
- Runtime helpers like `getSession`, `getContext`, `setContext`, `ensureContext`, `getSandbox`, and
40
- `getSkill` stay on the main `experimental-ash` barrel.
39
+ Runtime helpers like `getSession`, `getSandbox`, `getSkill`, `getContext`, `requireContext`, `hasContext`, `setContext`, and `ensureContext` stay on the
40
+ main `experimental-ash` barrel.
41
41
 
42
42
  ## What A Tool Definition Needs
43
43
 
@@ -106,8 +106,8 @@ execution surface by itself; executable behavior still comes from the tools the
106
106
 
107
107
  `read_file` reads a text file from the sandbox filesystem with line-numbered output. `write_file`
108
108
  writes a complete file, enforcing read-before-write for existing files and detecting stale reads.
109
- Both target the framework default sandbox. Compaction clears the durable read-file context so the
110
- model must re-read files before overwriting after compaction.
109
+ Both target the framework default sandbox. Compaction clears the durable read-file state so the model
110
+ must re-read files before overwriting after compaction.
111
111
 
112
112
  Authors can wrap, replace, or disable these defaults from `agent/tools/`.
113
113
 
@@ -225,23 +225,29 @@ export default defineWriteFileTool({
225
225
  ```
226
226
 
227
227
  When replacing the framework `read_file`, the authored version inherits the same compaction
228
- context-reset behavior by default. Provide an explicit `onCompact` to override that.
228
+ state-reset behavior by default. Provide an explicit `onCompact` to override that.
229
229
 
230
230
  ### Replace A Default With Custom Context
231
231
 
232
- `agent/tools/todo.ts` - replace the framework `todo` tool with a custom durable-context version.
233
- Declare your own `ContextKey` and use `ensureContext` / `setContext` to read and write it:
232
+ `agent/tools/todo.ts` - replace the framework `todo` tool with a custom durable context-backed
233
+ version. Declare your own `ContextKey`, initialize it with `ensureContext`, and read/write it with
234
+ `requireContext` / `setContext`:
234
235
 
235
236
  ```ts
236
- import { ContextKey, ensureContext, setContext } from "experimental-ash";
237
+ import {
238
+ ContextKey,
239
+ ensureContext,
240
+ requireContext,
241
+ setContext,
242
+ } from "experimental-ash";
237
243
  import { defineTool } from "experimental-ash/tools";
238
244
  import { z } from "zod";
239
245
 
240
- interface NoteListContext {
246
+ interface NoteListState {
241
247
  readonly notes: readonly string[];
242
248
  }
243
249
 
244
- const NoteListContextKey = new ContextKey<NoteListContext>("myapp.notes");
250
+ const NoteListStateKey = new ContextKey<NoteListState>("myapp.notes");
245
251
 
246
252
  export default defineTool({
247
253
  description: "Append a note or read the running list of notes.",
@@ -249,13 +255,15 @@ export default defineTool({
249
255
  note: z.string().optional(),
250
256
  }),
251
257
  async execute(input) {
258
+ ensureContext(NoteListStateKey, () => ({ notes: [] }));
259
+
252
260
  if (typeof input.note === "string" && input.note.length > 0) {
253
- const current = ensureContext(NoteListContextKey, () => ({ notes: [] }));
254
- setContext(NoteListContextKey, {
255
- notes: [...current.notes, input.note],
256
- });
261
+ const next = input.note;
262
+ setContext(NoteListStateKey, (current) => ({
263
+ notes: [...(current?.notes ?? []), next],
264
+ }));
257
265
  }
258
- return ensureContext(NoteListContextKey, () => ({ notes: [] }));
266
+ return requireContext(NoteListStateKey);
259
267
  },
260
268
  });
261
269
  ```
@@ -263,8 +271,8 @@ export default defineTool({
263
271
  ### Subtleties Worth Knowing
264
272
 
265
273
  - The filename is the default name. There is also an explicit `name` property when you need it.
266
- - Durable context is owned by the replacement. Replacing `todo` with a fresh `defineTool` does not
267
- inherit the framework's context key.
274
+ - Context is owned by the replacement. Replacing `todo` with a fresh `defineTool` does not inherit
275
+ the framework's durable context key.
268
276
  - Authored-vs-authored collisions are impossible by construction. Two files cannot share a slug under
269
277
  the same `agent/tools/` directory.
270
278
 
@@ -41,18 +41,16 @@ Most apps use `defineAgent`, `defineTool`, and `defineSandbox` the most.
41
41
  ## Runtime Helpers
42
42
 
43
43
  - `getSession()` - current session, turn, auth, and optional parent lineage
44
- - `getContext(key)` - read one durable authored context value
45
- - `setContext(key, value)` - write one durable authored context value
46
- - `ensureContext(key, valueOrFactory)` - read-or-create one durable context value
47
44
  - `getSandbox(name)` - live named sandbox session
48
45
  - `getSkill(identifier)` - handle for a named skill visible to the current agent
46
+ - `getContext(key)` / `requireContext(key)` / `hasContext(key)` / `setContext(key)` / `ensureContext(key, factory)` - unified context helpers
49
47
 
50
48
  Related exported types:
51
49
 
52
50
  - `Session`, `SessionAuth`, `SessionAuthContext`, `SessionParent`, `SessionTurn`
53
51
  - `SkillHandle`, `SkillResource`
54
52
  - `RunMode`
55
- - `ContextKey`, `ContextKeyOptions`, `AshContext`
53
+ - `ContextKey`, `ContextKeyOptions`, `ContextProvider`, `ContextReader`, `AshContext`
56
54
  - eval types from `experimental-ash/evals`
57
55
 
58
56
  Ash also exports lower-level runtime primitives such as `createToolLoopHarness(...)`,
@@ -63,8 +61,6 @@ sessions must finish inside the current invocation.
63
61
 
64
62
  These helpers only work inside active authored runtime execution.
65
63
 
66
- Breaking change details and before/after examples live in [`migration-guide.md`](./migration-guide.md).
67
-
68
64
  ## Markdown Helpers
69
65
 
70
66
  Ash also exports helpers for turning markdown into the same shapes as module-authored definitions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.2.0-alpha.21",
3
+ "version": "0.2.0-alpha.22",
4
4
  "type": "module",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",
@@ -1,18 +0,0 @@
1
- import type { HarnessSession } from "../harness/types.js";
2
- import type { AshContext } from "./container.js";
3
- /**
4
- * Hydrates all durable context values from `session.context`.
5
- *
6
- * Unknown keys are ignored. Keys without a persisted value remain absent until
7
- * authored code or a channel explicitly sets or ensures them.
8
- */
9
- export declare function hydrateDurableContext(ctx: AshContext, session: HarnessSession): Promise<void>;
10
- /**
11
- * Serializes all durable context values currently set on the context.
12
- */
13
- export declare function serializeDurableContext(ctx: AshContext): Record<string, unknown>;
14
- /**
15
- * Commits the full durable context bag back to `session.context`.
16
- */
17
- export declare function commitDurableContext(ctx: AshContext, session: HarnessSession): HarnessSession;
18
- //# sourceMappingURL=durable-context.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"durable-context.d.ts","sourceRoot":"","sources":["../../../src/context/durable-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYhF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,GAAG,cAAc,CAY7F"}
@@ -1,49 +0,0 @@
1
- import { isDeepStrictEqual } from "node:util";
2
- import { isDurableContextKey, resolveContextKey } from "./key.js";
3
- /**
4
- * Hydrates all durable context values from `session.context`.
5
- *
6
- * Unknown keys are ignored. Keys without a persisted value remain absent until
7
- * authored code or a channel explicitly sets or ensures them.
8
- */
9
- export async function hydrateDurableContext(ctx, session) {
10
- const data = session.context ?? {};
11
- for (const [name, raw] of Object.entries(data)) {
12
- if (raw === undefined) {
13
- continue;
14
- }
15
- const key = resolveContextKey(name);
16
- if (key === undefined) {
17
- continue;
18
- }
19
- ctx.set(key, key.codec ? await key.codec.deserialize(raw, ctx) : raw);
20
- }
21
- }
22
- /**
23
- * Serializes all durable context values currently set on the context.
24
- */
25
- export function serializeDurableContext(ctx) {
26
- const data = {};
27
- for (const [key, value] of ctx.entries()) {
28
- if (!isDurableContextKey(key) || value === undefined) {
29
- continue;
30
- }
31
- data[key.name] = key.codec ? key.codec.serialize(value) : value;
32
- }
33
- return data;
34
- }
35
- /**
36
- * Commits the full durable context bag back to `session.context`.
37
- */
38
- export function commitDurableContext(ctx, session) {
39
- const nextContext = serializeDurableContext(ctx);
40
- const currentContext = session.context ?? {};
41
- if (isDeepStrictEqual(nextContext, currentContext)) {
42
- return session;
43
- }
44
- return {
45
- ...session,
46
- context: Object.keys(nextContext).length > 0 ? nextContext : undefined,
47
- };
48
- }
49
- //# sourceMappingURL=durable-context.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"durable-context.js","sourceRoot":"","sources":["../../../src/context/durable-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAI9C,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAe,EACf,OAAuB;IAEvB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IAEnC,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxE,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAe;IACrD,MAAM,IAAI,GAA4B,EAAE,CAAC;IAEzC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACzC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACrD,SAAS;QACX,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAe,EAAE,OAAuB;IAC3E,MAAM,WAAW,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IAE7C,IAAI,iBAAiB,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO;QACL,GAAG,OAAO;QACV,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KACvE,CAAC;AACJ,CAAC"}
@@ -1,27 +0,0 @@
1
- import type { Channel, DeliverPayload, SessionAuthContext } from "../channel/types.js";
2
- import type { AshContext } from "../context/container.js";
3
- import type { HarnessSession, StepInput, StepResult } from "../harness/types.js";
4
- export interface StepDeliveryInput {
5
- readonly auth?: SessionAuthContext | null;
6
- readonly payload: DeliverPayload;
7
- }
8
- export interface PreparedStepContext {
9
- readonly channel: Channel;
10
- readonly channelStateSnapshot: Record<string, unknown>;
11
- readonly ctx: AshContext;
12
- readonly input: StepInput | undefined;
13
- }
14
- export declare function prepareStepContext(input: {
15
- readonly delivery?: StepDeliveryInput;
16
- readonly serializedRuntimeContext: Record<string, unknown>;
17
- readonly session: HarnessSession;
18
- }): Promise<PreparedStepContext>;
19
- /**
20
- * Runs one step inside the provider lifecycle and asserts that the
21
- * channel's serialized state was not mutated during execution.
22
- *
23
- * Combines {@link runStep} with a post-step channel stability check so
24
- * callers do not need to repeat the three-call sequence themselves.
25
- */
26
- export declare function runStepWithChannelGuard(prepared: PreparedStepContext, session: HarnessSession, callback: (session: HarnessSession) => Promise<StepResult>): Promise<StepResult>;
27
- //# sourceMappingURL=step-context.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"step-context.d.ts","sourceRoot":"","sources":["../../../src/execution/step-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAM1D,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CACvC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3D,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA0B/B;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,UAAU,CAAC,GACzD,OAAO,CAAC,UAAU,CAAC,CAQrB"}
@@ -1,54 +0,0 @@
1
- import { isDeepStrictEqual } from "node:util";
2
- import { hydrateDurableContext } from "../context/durable-context.js";
3
- import { AuthKey, ChannelKey } from "../context/keys.js";
4
- import { runStep } from "../context/run-step.js";
5
- import { deserializeRuntimeContext } from "../context/serialize.js";
6
- import { seedPendingInputRequestsContext } from "../harness/input-requests.js";
7
- export async function prepareStepContext(input) {
8
- const ctx = await deserializeRuntimeContext(input.serializedRuntimeContext);
9
- await hydrateDurableContext(ctx, input.session);
10
- seedPendingInputRequestsContext(ctx, input.session);
11
- if (input.delivery?.auth !== undefined) {
12
- ctx.set(AuthKey, input.delivery.auth ?? null);
13
- }
14
- const channel = ctx.get(ChannelKey);
15
- const channelStateSnapshot = channel.serialize();
16
- const resolved = input.delivery !== undefined ? await channel.onDeliver(ctx, input.delivery.payload) : undefined;
17
- assertChannelStateIsStable({
18
- channel,
19
- phase: "onDeliver",
20
- serializedState: channelStateSnapshot,
21
- });
22
- return {
23
- channel,
24
- channelStateSnapshot,
25
- ctx,
26
- input: resolved,
27
- };
28
- }
29
- /**
30
- * Runs one step inside the provider lifecycle and asserts that the
31
- * channel's serialized state was not mutated during execution.
32
- *
33
- * Combines {@link runStep} with a post-step channel stability check so
34
- * callers do not need to repeat the three-call sequence themselves.
35
- */
36
- export async function runStepWithChannelGuard(prepared, session, callback) {
37
- const stepResult = await runStep(prepared.ctx, session, callback);
38
- assertChannelStateIsStable({
39
- channel: prepared.channel,
40
- phase: "step execution",
41
- serializedState: prepared.channelStateSnapshot,
42
- });
43
- return stepResult;
44
- }
45
- function assertChannelStateIsStable(input) {
46
- const currentState = input.channel.serialize();
47
- if (isDeepStrictEqual(currentState, input.serializedState)) {
48
- return;
49
- }
50
- throw new Error(`Channel "${input.channel.kind}" mutated its serialized state during ${input.phase}. ` +
51
- "Channel constructor state must stay immutable after the run starts. " +
52
- "Move per-session mutable data into durable context or session.internal instead.");
53
- }
54
- //# sourceMappingURL=step-context.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"step-context.js","sourceRoot":"","sources":["../../../src/execution/step-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAI9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC;AAe/E,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,KAIxC;IACC,MAAM,GAAG,GAAG,MAAM,yBAAyB,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC5E,MAAM,qBAAqB,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAChD,+BAA+B,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAEpD,IAAI,KAAK,CAAC,QAAQ,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;QACvC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,oBAAoB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IACjD,MAAM,QAAQ,GACZ,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAElG,0BAA0B,CAAC;QACzB,OAAO;QACP,KAAK,EAAE,WAAW;QAClB,eAAe,EAAE,oBAAoB;KACtC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,oBAAoB;QACpB,GAAG;QACH,KAAK,EAAE,QAAQ;KAChB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,QAA6B,EAC7B,OAAuB,EACvB,QAA0D;IAE1D,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClE,0BAA0B,CAAC;QACzB,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,KAAK,EAAE,gBAAgB;QACvB,eAAe,EAAE,QAAQ,CAAC,oBAAoB;KAC/C,CAAC,CAAC;IACH,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,0BAA0B,CAAC,KAInC;IACC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;IAC/C,IAAI,iBAAiB,CAAC,YAAY,EAAE,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,IAAI,KAAK,CACb,YAAY,KAAK,CAAC,OAAO,CAAC,IAAI,yCAAyC,KAAK,CAAC,KAAK,IAAI;QACpF,sEAAsE;QACtE,iFAAiF,CACpF,CAAC;AACJ,CAAC"}
@@ -1,71 +0,0 @@
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
- ```