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.
- package/dist/src/channel/slack-channel.d.ts +7 -14
- package/dist/src/channel/slack-channel.d.ts.map +1 -1
- package/dist/src/channel/slack-channel.js +11 -21
- package/dist/src/channel/slack-channel.js.map +1 -1
- package/dist/src/channel/types.d.ts +9 -6
- package/dist/src/channel/types.d.ts.map +1 -1
- package/dist/src/channel/types.js.map +1 -1
- package/dist/src/context/accessors.d.ts +13 -12
- package/dist/src/context/accessors.d.ts.map +1 -1
- package/dist/src/context/accessors.js +16 -17
- package/dist/src/context/accessors.js.map +1 -1
- package/dist/src/context/container.d.ts +17 -19
- package/dist/src/context/container.d.ts.map +1 -1
- package/dist/src/context/container.js +21 -14
- package/dist/src/context/container.js.map +1 -1
- package/dist/src/context/durable-context.d.ts +18 -0
- package/dist/src/context/durable-context.d.ts.map +1 -0
- package/dist/src/context/durable-context.js +49 -0
- package/dist/src/context/durable-context.js.map +1 -0
- package/dist/src/context/key.d.ts +34 -82
- package/dist/src/context/key.d.ts.map +1 -1
- package/dist/src/context/key.js +58 -80
- package/dist/src/context/key.js.map +1 -1
- package/dist/src/context/keys.d.ts +6 -6
- package/dist/src/context/keys.d.ts.map +1 -1
- package/dist/src/context/keys.js +8 -6
- package/dist/src/context/keys.js.map +1 -1
- package/dist/src/context/provider.d.ts +2 -2
- package/dist/src/context/provider.d.ts.map +1 -1
- package/dist/src/context/run-step.d.ts +6 -8
- package/dist/src/context/run-step.d.ts.map +1 -1
- package/dist/src/context/run-step.js +13 -17
- package/dist/src/context/run-step.js.map +1 -1
- package/dist/src/context/seed-keys.d.ts +7 -7
- package/dist/src/context/seed-keys.d.ts.map +1 -1
- package/dist/src/context/seed-keys.js +19 -7
- package/dist/src/context/seed-keys.js.map +1 -1
- package/dist/src/context/serialize.d.ts +8 -9
- package/dist/src/context/serialize.d.ts.map +1 -1
- package/dist/src/context/serialize.js +28 -16
- package/dist/src/context/serialize.js.map +1 -1
- package/dist/src/execution/continuous-entry.d.ts +6 -8
- package/dist/src/execution/continuous-entry.d.ts.map +1 -1
- package/dist/src/execution/continuous-entry.js +25 -21
- package/dist/src/execution/continuous-entry.js.map +1 -1
- package/dist/src/execution/continuous-runtime.d.ts.map +1 -1
- package/dist/src/execution/continuous-runtime.js +4 -1
- package/dist/src/execution/continuous-runtime.js.map +1 -1
- package/dist/src/execution/step-context.d.ts +27 -0
- package/dist/src/execution/step-context.d.ts.map +1 -0
- package/dist/src/execution/step-context.js +54 -0
- package/dist/src/execution/step-context.js.map +1 -0
- package/dist/src/execution/tool-compaction.d.ts +1 -3
- package/dist/src/execution/tool-compaction.d.ts.map +1 -1
- package/dist/src/execution/tool-compaction.js +6 -17
- package/dist/src/execution/tool-compaction.js.map +1 -1
- package/dist/src/execution/workflow-entry.d.ts +4 -5
- package/dist/src/execution/workflow-entry.d.ts.map +1 -1
- package/dist/src/execution/workflow-entry.js.map +1 -1
- package/dist/src/execution/workflow-runtime.d.ts.map +1 -1
- package/dist/src/execution/workflow-runtime.js +3 -2
- package/dist/src/execution/workflow-runtime.js.map +1 -1
- package/dist/src/execution/workflow-steps.d.ts +2 -2
- package/dist/src/execution/workflow-steps.d.ts.map +1 -1
- package/dist/src/execution/workflow-steps.js +12 -20
- package/dist/src/execution/workflow-steps.js.map +1 -1
- package/dist/src/harness/emission.d.ts +1 -1
- package/dist/src/harness/emission.js +3 -3
- package/dist/src/harness/emission.js.map +1 -1
- package/dist/src/harness/input-requests.d.ts +22 -1
- package/dist/src/harness/input-requests.d.ts.map +1 -1
- package/dist/src/harness/input-requests.js +36 -32
- package/dist/src/harness/input-requests.js.map +1 -1
- package/dist/src/harness/types.d.ts +15 -3
- package/dist/src/harness/types.d.ts.map +1 -1
- package/dist/src/harness/types.js +6 -1
- package/dist/src/harness/types.js.map +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/routes/runtime-stack.d.ts +3 -3
- package/dist/src/internal/nitro/routes/runtime-stack.js +3 -3
- package/dist/src/public/channels/slack/index.d.ts +5 -0
- package/dist/src/public/channels/slack/index.d.ts.map +1 -1
- package/dist/src/public/channels/slack/index.js.map +1 -1
- package/dist/src/public/definitions/tool.d.ts +3 -4
- package/dist/src/public/definitions/tool.d.ts.map +1 -1
- package/dist/src/public/definitions/tool.js.map +1 -1
- package/dist/src/public/index.d.ts +1 -1
- package/dist/src/public/index.d.ts.map +1 -1
- package/dist/src/public/index.js +1 -1
- package/dist/src/public/index.js.map +1 -1
- package/dist/src/public/tools/defaults.d.ts +4 -4
- package/dist/src/public/tools/defaults.js +4 -4
- package/dist/src/runtime/framework-tools/connection-search.d.ts +2 -2
- package/dist/src/runtime/framework-tools/connection-search.d.ts.map +1 -1
- package/dist/src/runtime/framework-tools/connection-search.js +2 -2
- package/dist/src/runtime/framework-tools/connection-search.js.map +1 -1
- package/dist/src/runtime/framework-tools/file-state.d.ts +5 -5
- package/dist/src/runtime/framework-tools/file-state.d.ts.map +1 -1
- package/dist/src/runtime/framework-tools/file-state.js +6 -8
- package/dist/src/runtime/framework-tools/file-state.js.map +1 -1
- package/dist/src/runtime/framework-tools/skill.d.ts.map +1 -1
- package/dist/src/runtime/framework-tools/skill.js +1 -3
- package/dist/src/runtime/framework-tools/skill.js.map +1 -1
- package/dist/src/runtime/framework-tools/todo.d.ts.map +1 -1
- package/dist/src/runtime/framework-tools/todo.js +2 -4
- package/dist/src/runtime/framework-tools/todo.js.map +1 -1
- package/dist/src/runtime/sessions/auth.d.ts +1 -1
- package/docs/internals/context.md +81 -231
- package/docs/public/README.md +19 -17
- package/docs/public/channels/README.md +4 -0
- package/docs/public/migration-guide.md +71 -0
- package/docs/public/session-context.md +46 -17
- package/docs/public/tools.md +18 -27
- package/docs/public/typescript-api.md +5 -1
- package/package.json +1 -1
- package/dist/src/context/state.d.ts +0 -27
- package/dist/src/context/state.d.ts.map +0 -1
- package/dist/src/context/state.js +0 -53
- package/dist/src/context/state.js.map +0 -1
|
@@ -1,276 +1,126 @@
|
|
|
1
1
|
# Unified Context
|
|
2
2
|
|
|
3
|
-
Ash uses
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Core Model
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
44
|
-
tryGet<T>(key:
|
|
45
|
-
has(key:
|
|
46
|
-
set<T>(key:
|
|
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
|
-
|
|
27
|
+
Public helpers such as `getContext`, `setContext`, `ensureContext`, `getSession`, `getSandbox`, and
|
|
28
|
+
`getSkill` all delegate to this container.
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
call `contextStorage.run(...)`. Everything else reads via `requireContext()`.
|
|
30
|
+
## Durable Context Lifecycle
|
|
54
31
|
|
|
55
|
-
|
|
32
|
+
Durable authored context no longer piggybacks on runtime seed serialization.
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
model callbacks) uses this implicitly through the public accessors.
|
|
34
|
+
Every step now follows this flow:
|
|
59
35
|
|
|
60
|
-
|
|
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
|
-
|
|
43
|
+
The main files are:
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
explicit seed key list.
|
|
52
|
+
`serializeRuntimeContext(...)` and `deserializeRuntimeContext(...)` only handle
|
|
53
|
+
`RuntimeContextKey`s marked `serializable: true`.
|
|
83
54
|
|
|
84
|
-
|
|
55
|
+
That includes seed values such as:
|
|
85
56
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
71
|
+
- `SessionKey` via `sessionProvider`
|
|
72
|
+
- `SandboxKey` via `sandboxProvider`
|
|
73
|
+
- `SkillKey` via `skillProvider`
|
|
214
74
|
|
|
215
|
-
-
|
|
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
|
-
|
|
77
|
+
Provider-owned mutable state stays separate from authored durable context:
|
|
222
78
|
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
`packages/ash/src/execution/step-context.ts` owns the pre-step channel path for both runtimes:
|
|
231
86
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
94
|
+
That ordering matters. `onDeliver(...)` runs after durable context hydration, so channel-seeded
|
|
95
|
+
context cannot be overwritten by later hydration.
|
|
237
96
|
|
|
238
|
-
|
|
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
|
-
|
|
99
|
+
Serialized channel state is immutable after the run starts.
|
|
246
100
|
|
|
247
|
-
`
|
|
248
|
-
|
|
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
|
-
|
|
104
|
+
- durable context when authored code needs it across turns
|
|
105
|
+
- internal `session.internal` when only framework bookkeeping needs it
|
|
252
106
|
|
|
253
|
-
|
|
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
|
-
|
|
109
|
+
- transport normalization
|
|
110
|
+
- continuation token ownership
|
|
111
|
+
- per-turn context seeding
|
|
112
|
+
- delivery policy
|
|
258
113
|
|
|
259
|
-
|
|
114
|
+
It prevents channels from becoming an untracked second session storage system.
|
|
260
115
|
|
|
261
|
-
|
|
262
|
-
- `getSandbox(name)` reads `SandboxKey`
|
|
263
|
-
- `getSkill(identifier)` reads `SkillKey`
|
|
116
|
+
## Runtime Split
|
|
264
117
|
|
|
265
|
-
|
|
118
|
+
Both runtime flavors now use the same context story:
|
|
266
119
|
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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.
|
package/docs/public/README.md
CHANGED
|
@@ -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. [
|
|
23
|
-
6. [
|
|
24
|
-
7. [
|
|
25
|
-
8. [
|
|
26
|
-
9. [
|
|
27
|
-
10. [
|
|
28
|
-
11. [
|
|
29
|
-
12. [
|
|
30
|
-
13. [
|
|
31
|
-
14. [
|
|
32
|
-
15. [
|
|
33
|
-
16. [
|
|
34
|
-
17. [
|
|
35
|
-
18. [
|
|
36
|
-
19. [
|
|
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
|
|
60
|
+
- durable session context across turns
|
|
60
61
|
- a shared runtime workspace
|
|
61
62
|
- optional isolated sandboxes
|
|
62
|
-
- typed runtime helpers such as `getSession()`, `
|
|
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
|
|
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` (
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
208
|
-
channel, compiled bundle
|
|
209
|
-
2. Before each step,
|
|
210
|
-
|
|
211
|
-
3.
|
|
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
|
-
|
|
214
|
-
|
|
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.
|