experimental-ash 0.18.0 → 0.18.2

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 (72) hide show
  1. package/CHANGELOG.md +43 -2
  2. package/dist/docs/public/channels/README.md +75 -9
  3. package/dist/docs/public/schedules.md +13 -4
  4. package/dist/src/channel/adapter-context.d.ts +4 -0
  5. package/dist/src/channel/adapter-context.js +6 -0
  6. package/dist/src/channel/adapter.d.ts +10 -10
  7. package/dist/src/channel/cross-channel-receive.d.ts +1 -1
  8. package/dist/src/channel/cross-channel-receive.js +40 -0
  9. package/dist/src/channel/routes.d.ts +7 -0
  10. package/dist/src/channel/send.js +1 -1
  11. package/dist/src/channel/session.d.ts +47 -1
  12. package/dist/src/channel/session.js +46 -0
  13. package/dist/src/channel/types.d.ts +6 -5
  14. package/dist/src/chunks/client-CKsU8Li3.js +4 -0
  15. package/dist/src/chunks/{dev-authored-source-watcher-CG6kri3T.js → dev-authored-source-watcher-j7YWh2Gx.js} +1 -1
  16. package/dist/src/chunks/{host-CIU0NATc.js → host-DkTSR6YJ.js} +2 -2
  17. package/dist/src/chunks/{paths-CvbqpwTh.js → paths-Dwv0Eash.js} +22 -22
  18. package/dist/src/chunks/{prewarm-C_Vd0JR7.js → prewarm-CQYfka30.js} +1 -1
  19. package/dist/src/cli/commands/info.js +1 -1
  20. package/dist/src/cli/dev/repl.js +1 -1
  21. package/dist/src/cli/run.js +1 -1
  22. package/dist/src/client/client.js +2 -1
  23. package/dist/src/client/index.d.ts +3 -0
  24. package/dist/src/client/index.js +1 -0
  25. package/dist/src/client/message-reducer-types.d.ts +130 -0
  26. package/dist/src/client/message-reducer-types.js +1 -0
  27. package/dist/src/client/message-reducer.d.ts +14 -0
  28. package/dist/src/client/message-reducer.js +462 -0
  29. package/dist/src/client/open-stream.js +2 -4
  30. package/dist/src/client/reducer.d.ts +63 -0
  31. package/dist/src/client/reducer.js +1 -0
  32. package/dist/src/client/session.js +3 -5
  33. package/dist/src/client/url.d.ts +8 -0
  34. package/dist/src/client/url.js +34 -0
  35. package/dist/src/compiler/module-map.js +12 -0
  36. package/dist/src/evals/cli/eval.js +1 -1
  37. package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
  38. package/dist/src/execution/sandbox/bindings/vercel.js +1 -34
  39. package/dist/src/execution/workflow-entry.js +35 -31
  40. package/dist/src/execution/workflow-steps.d.ts +16 -0
  41. package/dist/src/execution/workflow-steps.js +32 -4
  42. package/dist/src/harness/attachment-staging.js +2 -1
  43. package/dist/src/internal/application/package.js +1 -1
  44. package/dist/src/public/channels/slack/api.d.ts +13 -8
  45. package/dist/src/public/channels/slack/api.js +31 -17
  46. package/dist/src/public/channels/slack/index.d.ts +2 -2
  47. package/dist/src/public/channels/slack/index.js +1 -0
  48. package/dist/src/public/channels/slack/interactions.js +3 -3
  49. package/dist/src/public/channels/slack/slackChannel.d.ts +5 -3
  50. package/dist/src/public/channels/slack/slackChannel.js +26 -15
  51. package/dist/src/public/channels/twilio/api.d.ts +9 -0
  52. package/dist/src/public/channels/twilio/api.js +11 -0
  53. package/dist/src/public/channels/twilio/index.d.ts +1 -1
  54. package/dist/src/public/channels/twilio/index.js +1 -1
  55. package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
  56. package/dist/src/public/channels/twilio/twilioChannel.js +8 -11
  57. package/dist/src/public/definitions/defineChannel.d.ts +9 -1
  58. package/dist/src/public/definitions/defineChannel.js +7 -11
  59. package/dist/src/public/definitions/sandbox.d.ts +2 -3
  60. package/dist/src/public/sandbox/backends/vercel.d.ts +4 -4
  61. package/dist/src/public/sandbox/backends/vercel.js +2 -2
  62. package/dist/src/public/sandbox/index.d.ts +1 -1
  63. package/dist/src/public/sandbox/vercel-sandbox.d.ts +3 -28
  64. package/dist/src/react/index.d.ts +3 -0
  65. package/dist/src/react/index.js +3 -0
  66. package/dist/src/react/use-ash-agent.d.ts +79 -0
  67. package/dist/src/react/use-ash-agent.js +330 -0
  68. package/dist/src/runtime/types.d.ts +1 -2
  69. package/dist/src/shared/sandbox-backend.d.ts +4 -4
  70. package/dist/src/shared/sandbox-definition.d.ts +6 -6
  71. package/package.json +15 -2
  72. package/dist/src/chunks/client-BeZ_W7vl.js +0 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.18.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fd90ad7: fix(ash): ensure `SessionUseFn` returns a `SandboxSession` for consistency with `BootstrapUseFn`
8
+ - cd3d65d: Fix Slack sessions that start without a `threadTs` (programmatic
9
+ `args.receive(slack, ...)`, schedule fires, etc.). Before this fix
10
+ each agent reply landed as a separate top-level channel message,
11
+ typing indicators silently no-opped, and follow-up `@mention`
12
+ replies started fresh sessions. The channel now auto-anchors on the
13
+ first agent post: the post's `ts` becomes the thread root, every
14
+ subsequent post and typing indicator threads under it, and the
15
+ parked session is re-keyed to `slack:<channelId>:<anchored-ts>` so a
16
+ follow-up mention resumes the same Ash session.
17
+ - a4b3837: rename slack channel initialPost to initialMessage
18
+
19
+ ## 0.18.1
20
+
21
+ ### Patch Changes
22
+
23
+ - 0d710b6: Fix `ResolveAgentError: Missing compiled module namespace for schedule
24
+ source ...` when a Nitro cron fires a TypeScript schedule that declares
25
+ an `async run({ ... })` handler.
26
+
27
+ The compiler's runtime module map (`.ash/compile/module-map.mjs`) lists
28
+ every module-backed authored source the runtime needs to load lazily.
29
+ It already collected channels, connections, tools, hooks, the sandbox,
30
+ the agent config, and the model — but it silently dropped schedules.
31
+
32
+ Markdown schedules (and TypeScript schedules whose only body is a
33
+ `markdown` string) execute via the pre-compiled `markdown` captured in
34
+ the manifest, so they never tripped the gap. Schedules with a `run`
35
+ handler, on the other hand, must load their default export at dispatch
36
+ time — and that load went straight to `ResolveAgentError` because the
37
+ module map had no entry for `schedules/<name>.ts`.
38
+
39
+ Module-sourced schedules with `hasRun: true` are now included in the
40
+ compiled module map. Markdown schedules and `markdown`-only module
41
+ schedules are still omitted (their body lives in the manifest and the
42
+ dispatcher never loads the source).
43
+
3
44
  ## 0.18.0
4
45
 
5
46
  ### Minor Changes
@@ -63,9 +104,9 @@
63
104
  continuation-token format and initial state; `auth` flows through to
64
105
  `session.initiatorAuth`.
65
106
 
66
- `slackChannel().receive` accepts an optional `initialPost` so callers
107
+ `slackChannel().receive` accepts an optional `initialMessage` so callers
67
108
  can post an anchor card before the agent runs; the agent's first turn
68
- threads under that card. `threadTs` and `initialPost` are mutually
109
+ threads under that card. `threadTs` and `initialMessage` are mutually
69
110
  exclusive.
70
111
 
71
112
  The schedule dispatcher's receive invocation now shares its
@@ -297,8 +297,20 @@ Semantics:
297
297
  - The first argument is the target channel module's default export -- import it directly
298
298
  from `agent/channels/<name>.ts`. Identity is matched by reference.
299
299
 
300
- Slack's `receive` accepts an optional `initialPost` so the target channel can post a card
301
- before the agent runs; subsequent agent turns thread under that card.
300
+ ### Slack thread anchoring
301
+
302
+ When a Slack session starts without a `threadTs` (programmatic `args.receive(slack, ...)`,
303
+ schedule fires, etc.), the channel **auto-anchors on the first agent post**: the message
304
+ becomes the thread root, and every subsequent post, typing indicator, and inbound
305
+ `@mention` reply in that thread resumes the same Ash session. Schedule digests, webhook
306
+ hand-offs, and other session-from-nothing flows produce clean Slack threads with no extra
307
+ wiring — the channel passes the new channel-local token to
308
+ `ctx.session.setContinuationToken(...)`, and the runtime re-keys the parked session to
309
+ `slack:<channelId>:<anchored-ts>` so follow-up mentions land back in the same workflow.
310
+
311
+ Pass `initialMessage` when you want a structured anchor card to land _before_ the agent
312
+ runs — useful for "Investigation Thread for INC-42"-style banners that should precede
313
+ the model's first reply:
302
314
 
303
315
  ```ts
304
316
  import { Card, CardText } from "experimental-ash/channels/slack";
@@ -307,7 +319,7 @@ await args.receive(slack, {
307
319
  message: "Begin investigation",
308
320
  args: {
309
321
  channelId: "C0123ABC",
310
- initialPost: {
322
+ initialMessage: {
311
323
  card: Card({ children: [CardText("Investigation Thread for INC-42")] }),
312
324
  fallbackText: "Investigation Thread for INC-42",
313
325
  },
@@ -316,13 +328,67 @@ await args.receive(slack, {
316
328
  });
317
329
  ```
318
330
 
319
- `threadTs` and `initialPost` are mutually exclusive: pass `threadTs` to join an existing
320
- thread, or `initialPost` to anchor a new one.
331
+ `threadTs` and `initialMessage` are mutually exclusive: pass `threadTs` to join an existing
332
+ thread, or `initialMessage` to anchor a new one. With neither, the first agent post anchors
333
+ the thread automatically.
334
+
335
+ ## Continuation Tokens
336
+
337
+ Each call to `send(message, { auth, continuationToken, state? })` from a channel route
338
+ addresses a session by its **channel-local raw token**. The framework prepends the
339
+ channel name (the file stem under `agent/channels/`) before handing the token to the
340
+ runtime, so a Slack route that passes `"C0123ABC:1800000000.001234"` ends up addressing
341
+ session `"slack:C0123ABC:1800000000.001234"`.
342
+
343
+ Authored channels typically ship a small helper for building the token:
344
+
345
+ ```ts
346
+ import { slackContinuationToken } from "experimental-ash/channels/slack";
347
+ import { twilioContinuationToken } from "experimental-ash/channels/twilio";
348
+
349
+ slackContinuationToken("C0123ABC", "1800000000.001234"); // "C0123ABC:1800000000.001234"
350
+ twilioContinuationToken("+15551234567", "+15557654321"); // "+15551234567:+15557654321"
351
+ ```
352
+
353
+ Custom channels just write a function that joins their identity fields. The framework
354
+ does not derive anything for you — the channel owns its token format.
355
+
356
+ ### Re-keying mid-session
357
+
358
+ When the identity that should address a session isn't known until the first agent post
359
+ (Slack's auto-anchor: the post's `ts` becomes the thread root), the channel re-keys the
360
+ parked session by calling `ctx.session.setContinuationToken(...)` from a handler. Pass
361
+ the **channel-local raw token**; the runtime preserves the current channel namespace:
362
+
363
+ ```ts
364
+ import { defineChannel } from "experimental-ash/channels";
365
+
366
+ defineChannel<{ ref: string | null }>({
367
+ state: { ref: null },
368
+ context(state, session) {
369
+ return {
370
+ state,
371
+ registerAnchor(ref: string) {
372
+ state.ref = ref;
373
+ session.setContinuationToken(ref);
374
+ },
375
+ };
376
+ },
377
+ events: {
378
+ "message.completed"(_event, ctx) {
379
+ if (!ctx.state.ref) ctx.registerAnchor(mintRef());
380
+ },
381
+ },
382
+ routes: [
383
+ /* ... */
384
+ ],
385
+ });
386
+ ```
321
387
 
322
- The card's `ts` becomes the session's continuation token, so any later `@mention` reply
323
- inside that thread resumes the same session instead of starting a new one. Without
324
- `initialPost`, programmatically-started Slack sessions have no thread to anchor under and
325
- follow-up mentions land in a fresh session.
388
+ The workflow runtime disposes the current park hook at the next step boundary and
389
+ registers a new one at the new token. Inbound deliveries already addressed to the old
390
+ token are dropped coordinate with your senders so follow-up traffic uses the new
391
+ token.
326
392
 
327
393
  ## File Uploads
328
394
 
@@ -46,9 +46,18 @@ export default defineSchedule({
46
46
  - `waitUntil(promise)` — extends the cron task's lifetime past handler return so in-flight work settles before the Nitro task completes.
47
47
  - `appAuth` — pre-built APP auth context (`{ authenticator: "app", principalId: "ash:app", principalType: "runtime" }`). Pass to `receive(..., { auth: appAuth })` for schedules that run on behalf of the agent itself.
48
48
 
49
- #### Announce-then-deliver
49
+ #### Slack thread anchoring
50
50
 
51
- A handler that hands off to Slack can post an anchor card before the agent runs so the thread shows "what this is about" up front; the agent's reply then lands threaded under that card. Same `initialPost` field [Slack's `receive`](./channels/README.md#cross-channel-hand-off) accepts from a route handler:
51
+ A schedule that hands off to Slack does not need a pre-existing thread. The channel
52
+ [auto-anchors](./channels/README.md#slack-thread-anchoring) on the first agent post:
53
+ that message becomes the thread root, the channel calls
54
+ `ctx.session.setContinuationToken(...)` with the channel-local anchor token, and the
55
+ runtime re-keys the parked session under the namespaced Slack token. The rest of the
56
+ turn (plus any future `@mention` reply) threads under it. A digest that sparks a
57
+ question can be answered in-thread without spinning up a new session.
58
+
59
+ Pass `initialMessage` when you want a structured banner card to land _before_ the agent
60
+ runs — useful for "Daily Deploy Digest"-style headers:
52
61
 
53
62
  ```ts
54
63
  // agent/schedules/deploy-digest.ts
@@ -65,7 +74,7 @@ export default defineSchedule({
65
74
  message: "Summarize today's production deploys.",
66
75
  args: {
67
76
  channelId: "C0123ABC",
68
- initialPost: {
77
+ initialMessage: {
69
78
  card: Card({ children: [CardText("Daily Deploy Digest")] }),
70
79
  fallbackText: "Daily Deploy Digest",
71
80
  },
@@ -77,7 +86,7 @@ export default defineSchedule({
77
86
  });
78
87
  ```
79
88
 
80
- `threadTs` and `initialPost` are mutually exclusive on Slack receive args. The anchor card's `ts` becomes the session's continuation token, so any later `@mention` reply in that thread resumes the same session — a digest that sparks a question can be answered in-thread without spinning up a new session.
89
+ `threadTs` and `initialMessage` are mutually exclusive on Slack receive args.
81
90
 
82
91
  ### Markdown (`markdown`)
83
92
 
@@ -3,5 +3,9 @@ import type { ChannelAdapter, ChannelAdapterContext } from "#channel/adapter.js"
3
3
  /**
4
4
  * Builds the {@link ChannelAdapterContext} the runtime hands to an
5
5
  * adapter's `deliver` hook, event handlers, and attachment resolver.
6
+ *
7
+ * Populates `session` with a live {@link SessionHandle} backed by the
8
+ * supplied accessor so handlers can read identity / auth and call
9
+ * `setContinuationToken(...)` to re-key the parked session.
6
10
  */
7
11
  export declare function buildAdapterContext<TCtx extends ChannelAdapterContext<any> = ChannelAdapterContext>(adapter: ChannelAdapter<TCtx>, accessor: ContextAccessor): TCtx;
@@ -1,6 +1,11 @@
1
+ import { buildSessionHandle } from "#channel/session.js";
1
2
  /**
2
3
  * Builds the {@link ChannelAdapterContext} the runtime hands to an
3
4
  * adapter's `deliver` hook, event handlers, and attachment resolver.
5
+ *
6
+ * Populates `session` with a live {@link SessionHandle} backed by the
7
+ * supplied accessor so handlers can read identity / auth and call
8
+ * `setContinuationToken(...)` to re-key the parked session.
4
9
  */
5
10
  export function buildAdapterContext(adapter, accessor) {
6
11
  // `adapter.state` is stored loosely (`Record<string, unknown>`)
@@ -11,6 +16,7 @@ export function buildAdapterContext(adapter, accessor) {
11
16
  const baseCtx = {
12
17
  ctx: accessor,
13
18
  state: adapter.state ?? {},
19
+ session: buildSessionHandle(accessor),
14
20
  };
15
21
  return adapter.createAdapterContext
16
22
  ? adapter.createAdapterContext(baseCtx)
@@ -2,6 +2,7 @@ import type { ContextAccessor } from "#context/key.js";
2
2
  import type { ContextProvider } from "#context/provider.js";
3
3
  import type { StepInput } from "#harness/types.js";
4
4
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
5
+ import type { SessionHandle } from "#channel/session.js";
5
6
  import type { DeliverPayload } from "#channel/types.js";
6
7
  /**
7
8
  * Context available to every adapter handler (`deliver` and event handlers).
@@ -11,6 +12,11 @@ import type { DeliverPayload } from "#channel/types.js";
11
12
  *
12
13
  * `ctx` provides read/write access to durable context keys (the same
13
14
  * {@link ContextAccessor} that tools and providers use).
15
+ *
16
+ * `session` is a live handle to the current session — id, auth,
17
+ * continuation token, plus an imperative {@link SessionHandle.setContinuationToken}
18
+ * for channels that need to re-key the session mid-turn (e.g. Slack's
19
+ * auto-anchor on first post).
14
20
  */
15
21
  export interface ChannelAdapterContext<TState = Record<string, unknown>> {
16
22
  /**
@@ -23,6 +29,10 @@ export interface ChannelAdapterContext<TState = Record<string, unknown>> {
23
29
  * to read during the turn.
24
30
  */
25
31
  readonly ctx: ContextAccessor;
32
+ /**
33
+ * Live handle to the current session.
34
+ */
35
+ readonly session: SessionHandle;
26
36
  }
27
37
  /**
28
38
  * Extracts the state type from a {@link ChannelAdapterContext} generic.
@@ -121,16 +131,6 @@ export type ChannelAdapter<TCtx extends ChannelAdapterContext<any> = ChannelAdap
121
131
  * construction time.
122
132
  */
123
133
  readonly fetchFile?: (url: string) => Promise<Buffer | FetchFileResult | null>;
124
- /**
125
- * Derives the continuation token from adapter state.
126
- *
127
- * The runtime calls this at park time when no explicit
128
- * `continuationToken` was provided on {@link RunInput}. Returns
129
- * `undefined` when the adapter's state does not yet contain enough
130
- * information to form a token (e.g., a Slack adapter whose first
131
- * post has not yet created the thread).
132
- */
133
- getContinuationToken?(): string | undefined;
134
134
  } & ChannelEventHandlers<TCtx>;
135
135
  /**
136
136
  * Produces the default {@link StepInput} when no custom adapter `deliver`
@@ -1,6 +1,6 @@
1
1
  import type { UserContent } from "ai";
2
2
  import type { ChannelAdapter } from "#channel/adapter.js";
3
- import type { CompiledChannel } from "#channel/compiled-channel.js";
3
+ import { type CompiledChannel } from "#channel/compiled-channel.js";
4
4
  import type { InferReceiveArgs } from "#channel/receive-args.js";
5
5
  import type { Session } from "#channel/session.js";
6
6
  import type { Runtime, SessionAuthContext } from "#channel/types.js";
@@ -1,3 +1,4 @@
1
+ import { isCompiledChannel } from "#channel/compiled-channel.js";
1
2
  import { createSendFn } from "#channel/send.js";
2
3
  /**
3
4
  * Builds the `args.receive` closure used by every route handler. The
@@ -44,7 +45,46 @@ function resolveTargetByReference(ref, channels) {
44
45
  return channel;
45
46
  }
46
47
  }
48
+ const structurallyMatchedTarget = resolveTargetByRouteFingerprint(ref, channels);
49
+ if (structurallyMatchedTarget !== null) {
50
+ return structurallyMatchedTarget;
51
+ }
47
52
  throw new Error("args.receive(): the channel passed as the first argument is not registered " +
48
53
  "in this agent's channels/. Import the channel module's default export from " +
49
54
  "agent/channels/<name>.ts and pass that value.");
50
55
  }
56
+ function resolveTargetByRouteFingerprint(ref, channels) {
57
+ if (!isCompiledChannel(ref)) {
58
+ return null;
59
+ }
60
+ const refFingerprint = createRouteFingerprint(ref);
61
+ if (refFingerprint === null) {
62
+ return null;
63
+ }
64
+ const matches = new Map();
65
+ for (const channel of channels) {
66
+ const definition = channel.definition;
67
+ if (definition === undefined || createRouteFingerprint(definition) !== refFingerprint) {
68
+ continue;
69
+ }
70
+ matches.set(channel.name, channel);
71
+ }
72
+ if (matches.size === 1) {
73
+ return [...matches.values()][0];
74
+ }
75
+ if (matches.size > 1) {
76
+ throw new Error("args.receive(): the channel passed as the first argument matches multiple " +
77
+ "registered channels by route shape. Import a channel with a unique route set " +
78
+ "from agent/channels/<name>.ts before passing it to args.receive().");
79
+ }
80
+ return null;
81
+ }
82
+ function createRouteFingerprint(channel) {
83
+ if (channel.routes.length === 0) {
84
+ return null;
85
+ }
86
+ const routes = channel.routes
87
+ .map((route) => `${route.method.toUpperCase()} ${route.path}`)
88
+ .sort();
89
+ return routes.join("\n");
90
+ }
@@ -23,6 +23,13 @@ export interface SendPayload {
23
23
  readonly inputResponses?: readonly InputResponse[];
24
24
  }
25
25
  export type SendFn<TState = undefined> = (input: string | UserContent | SendPayload, options: SendOptions<TState>) => Promise<Session>;
26
+ /**
27
+ * Options for {@link SendFn}. The channel owns its continuation-token
28
+ * format — pass the channel-local raw token (the framework prepends
29
+ * the channel name). Stateful channels also seed initial adapter
30
+ * state via {@link state}, which becomes the new session's `state`
31
+ * on first `runtime.run()` and is ignored on subsequent `deliver`s.
32
+ */
26
33
  export type SendOptions<TState = undefined> = TState extends undefined ? {
27
34
  auth: SessionAuthContext | null;
28
35
  continuationToken: string;
@@ -7,9 +7,9 @@ const log = createLogger("channel.send");
7
7
  export function createSendFn(runtime, adapter, channelName) {
8
8
  return async (input, options) => {
9
9
  const auth = options.auth;
10
+ const state = options.state;
10
11
  const rawToken = options.continuationToken;
11
12
  const continuationToken = `${channelName}:${rawToken}`;
12
- const state = options.state;
13
13
  const { message: rawMessage, inputResponses } = normalizeSendInput(input);
14
14
  const message = serializeUrlFilePartsInMessage(rawMessage);
15
15
  try {
@@ -1,3 +1,4 @@
1
+ import type { ContextAccessor } from "#context/key.js";
1
2
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
2
3
  import type { Runtime, SessionAuthContext } from "#channel/types.js";
3
4
  export interface Session {
@@ -7,12 +8,57 @@ export interface Session {
7
8
  startIndex?: number;
8
9
  }): Promise<ReadableStream<HandleMessageStreamEvent>>;
9
10
  }
11
+ /**
12
+ * Live handle to the current session, exposed on `ctx.session` to
13
+ * `deliver` and event handlers. The framework hydrates the read-only
14
+ * fields from the active context at step start; mutations made through
15
+ * {@link SessionHandle.setContinuationToken} flow back through the
16
+ * context so the runtime can re-key the parked workflow hook at the
17
+ * next step boundary.
18
+ */
10
19
  export interface SessionHandle {
11
20
  readonly id: string;
21
+ /**
22
+ * Runtime-scoped continuation token (`<channelName>:<channel-local-token>`).
23
+ */
12
24
  readonly continuationToken: string;
13
25
  readonly auth: SessionAuthContext | null;
14
26
  readonly initiatorAuth: SessionAuthContext | null;
15
- setContinuationToken(token: string): void;
27
+ /**
28
+ * Re-key the session under a new continuation token.
29
+ *
30
+ * Use this when the channel's resume address depends on data
31
+ * produced during the turn (e.g. Slack auto-anchoring its first
32
+ * post adopts the post's `ts` as the thread root). Pass the
33
+ * channel-local raw token, matching the token shape accepted by
34
+ * route `send()`; the session handle preserves the current channel
35
+ * namespace before writing to runtime context.
36
+ *
37
+ * Effects:
38
+ *
39
+ * - Updates `ContinuationTokenKey` in the active context to the
40
+ * runtime-scoped token (`<channelName>:<rawToken>`).
41
+ * - Causes the workflow runtime to dispose its current park hook
42
+ * and register a new one at the new token at the next step
43
+ * boundary, so follow-up `deliver` calls keyed under the new
44
+ * token resume the same session.
45
+ * - Idempotent — calling with the current token is a no-op.
46
+ *
47
+ * The session must already have a namespaced placeholder
48
+ * continuation token so the handle can preserve the channel name.
49
+ */
50
+ setContinuationToken(rawToken: string): void;
16
51
  }
17
52
  export declare function createSession(id: string, continuationToken: string, runtime: Runtime): Session;
18
53
  export declare function createGetSessionFn(runtime: Runtime): (sessionId: string) => Session;
54
+ /**
55
+ * Builds a live {@link SessionHandle} backed by the active context
56
+ * accessor. Read-only fields resolve through getters so they reflect
57
+ * any updates made by other handlers within the same step (e.g. the
58
+ * `deliver` hook seeding `AuthKey` before an event handler reads
59
+ * `session.auth`).
60
+ *
61
+ * Used by {@link buildAdapterContext} to populate `ctx.session` on
62
+ * every adapter handler invocation.
63
+ */
64
+ export declare function buildSessionHandle(accessor: ContextAccessor): SessionHandle;
@@ -1,3 +1,4 @@
1
+ import { AuthKey, ContinuationTokenKey, InitiatorAuthKey, SessionIdKey, } from "#context/seed-keys.js";
1
2
  export function createSession(id, continuationToken, runtime) {
2
3
  return {
3
4
  id,
@@ -10,3 +11,48 @@ export function createSession(id, continuationToken, runtime) {
10
11
  export function createGetSessionFn(runtime) {
11
12
  return (sessionId) => createSession(sessionId, "", runtime);
12
13
  }
14
+ /**
15
+ * Builds a live {@link SessionHandle} backed by the active context
16
+ * accessor. Read-only fields resolve through getters so they reflect
17
+ * any updates made by other handlers within the same step (e.g. the
18
+ * `deliver` hook seeding `AuthKey` before an event handler reads
19
+ * `session.auth`).
20
+ *
21
+ * Used by {@link buildAdapterContext} to populate `ctx.session` on
22
+ * every adapter handler invocation.
23
+ */
24
+ export function buildSessionHandle(accessor) {
25
+ return {
26
+ get id() {
27
+ return accessor.get(SessionIdKey) ?? "";
28
+ },
29
+ get continuationToken() {
30
+ return accessor.get(ContinuationTokenKey) ?? "";
31
+ },
32
+ get auth() {
33
+ return accessor.get(AuthKey) ?? null;
34
+ },
35
+ get initiatorAuth() {
36
+ return accessor.get(InitiatorAuthKey) ?? null;
37
+ },
38
+ setContinuationToken(rawToken) {
39
+ const currentToken = accessor.get(ContinuationTokenKey) ?? "";
40
+ const token = namespaceContinuationToken(currentToken, rawToken);
41
+ // Idempotent: a redundant write would push the workflow body
42
+ // through a hook dispose / recreate cycle for no reason. The
43
+ // call must remain cheap so channels can call it from
44
+ // hot-path event handlers without measuring first.
45
+ if (currentToken === token)
46
+ return;
47
+ accessor.set(ContinuationTokenKey, token);
48
+ },
49
+ };
50
+ }
51
+ function namespaceContinuationToken(currentToken, rawToken) {
52
+ const separatorIndex = currentToken.indexOf(":");
53
+ if (separatorIndex <= 0) {
54
+ throw new Error("Cannot set session continuation token without an existing namespaced " +
55
+ "continuation token. Start the session with a placeholder continuationToken.");
56
+ }
57
+ return `${currentToken.slice(0, separatorIndex + 1)}${rawToken}`;
58
+ }
@@ -160,11 +160,12 @@ export interface RunInput {
160
160
  */
161
161
  readonly capabilities?: SessionCapabilities;
162
162
  /**
163
- * Session continuation token for delivery and hook creation. When
164
- * omitted, the runtime falls back to the adapter's
165
- * `getContinuationToken()` at park time. This supports
166
- * schedule-initiated sessions where the token depends on data
167
- * produced during the first turn (e.g., the Slack thread `ts`).
163
+ * Session continuation token for delivery and hook creation. Channels
164
+ * can re-key the session during the first turn by calling
165
+ * `ctx.session.setContinuationToken(...)` (e.g. Slack auto-anchoring
166
+ * its first post adopts the post's `ts` as the thread root), so an
167
+ * initial placeholder token is acceptable when full identity isn't
168
+ * known until the channel emits its first message.
168
169
  */
169
170
  readonly continuationToken?: string;
170
171
  /**
@@ -0,0 +1,4 @@
1
+ import{i as e,t}from"./chunk-8L7ocgPr.js";import{_ as n,f as r,g as i,l as a,p as o,v as s}from"./types-MZUhN0Zy.js";import{n as c,r as l,t as u}from"./token-util-CHjOk3A7.js";var d=class extends Error{status;body;constructor(e,t){super(t||`Server returned ${e}.`),this.name=`ClientError`,this.status=e,this.body=t}};function f(e){if(e instanceof DOMException)return e.name===`AbortError`;if(!(e instanceof Error))return!1;let t=`code`in e&&typeof e.code==`string`?e.code:void 0;return e.name===`AbortError`||e.message===`terminated`||t===`UND_ERR_SOCKET`||/abort|cancel|disconnect|premature close|socket|terminated/i.test(e.message)}async function*p(e){let t=e.getReader(),n=new TextDecoder,r=``;try{for(;;){let e=await t.read();if(e.done){r+=n.decode();break}e.value&&(r+=n.decode(e.value,{stream:!0}));let i=r.indexOf(`
2
+ `);for(;i!==-1;){let e=r.slice(0,i).trim();r=r.slice(i+1),e.length>0&&(yield JSON.parse(e)),i=r.indexOf(`
3
+ `)}}let e=r.trim();e.length>0&&(yield JSON.parse(e))}finally{t.releaseLock()}}function m(e,t,n){let r=t.startsWith(`/`)?t:`/${t}`,i=_(n);if(h(e)){let t=new URL(e);return t.pathname=`${g(t.pathname)}${r}`,t.search=i,t.hash=``,t.toString()}return`${g(e)}${r}${i}`}function h(e){return/^[a-z][a-z\d+\-.]*:/i.test(e)}function g(e){return e===`/`?``:e.endsWith(`/`)?e.slice(0,-1):e}function _(e){return!e||Object.keys(e).length===0?``:`?${new URLSearchParams(e).toString()}`}async function*v(e){let t=e.startIndex,n=e.maxReconnectAttempts;for(;;){let r=m(e.host,s(e.sessionId),t>0?{startIndex:String(t)}:void 0),i=await e.resolveHeaders(),a=await fetch(r,{headers:i,signal:e.signal??null});if(!a.ok){let e=await a.text();throw new d(a.status,e)}if(!a.body)throw new d(a.status,`Response body is null.`);let o=!1;try{for await(let e of p(a.body))t+=1,yield e}catch(e){if(!f(e))throw e;o=!0}if(!o||n<=0)return;--n}}var y=t(((e,t)=>{var n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.prototype.hasOwnProperty,o=(e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})},s=(e,t,o,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(let c of i(t))!a.call(e,c)&&c!==o&&n(e,c,{get:()=>t[c],enumerable:!(s=r(t,c))||s.enumerable});return e},c=e=>s(n({},`__esModule`,{value:!0}),e),l={};o(l,{SYMBOL_FOR_REQ_CONTEXT:()=>u,getContext:()=>d}),t.exports=c(l);let u=Symbol.for(`@vercel/request-context`);function d(){return globalThis[u]?.get?.()??{}}})),b=t(((t,n)=>{var r=Object.defineProperty,i=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,o=Object.prototype.hasOwnProperty,s=(e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})},c=(e,t,n,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(let c of a(t))!o.call(e,c)&&c!==n&&r(e,c,{get:()=>t[c],enumerable:!(s=i(t,c))||s.enumerable});return e},u=e=>c(r({},`__esModule`,{value:!0}),e),d={};s(d,{getVercelOidcToken:()=>m,getVercelOidcTokenSync:()=>h}),n.exports=u(d);var f=y(),p=l();async function m(t){let n=``,r;try{n=h()}catch(e){r=e}try{let[{getTokenPayload:r,isExpired:i},{refreshToken:a}]=await Promise.all([await import(`./token-util-CHjOk3A7.js`).then(t=>e(t.t())),await import(`./token-DtoyQZy2.js`).then(t=>e(t.default))]);(!n||i(r(n),t?.expirationBufferMs))&&(await a(t),n=h())}catch(e){let t=r instanceof Error?r.message:``;throw e instanceof Error&&(t=`${t}
4
+ ${e.message}`),t?new p.VercelOidcTokenError(t):e}return n}function h(){let e=(0,f.getContext)().headers?.[`x-vercel-oidc-token`]??process.env.VERCEL_OIDC_TOKEN;if(!e)throw Error(`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`);return e}})),x=t(((e,t)=>{var n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.prototype.hasOwnProperty,o=(e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})},s=(e,t,o,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(let c of i(t))!a.call(e,c)&&c!==o&&n(e,c,{get:()=>t[c],enumerable:!(s=r(t,c))||s.enumerable});return e},l=e=>s(n({},`__esModule`,{value:!0}),e),d={};o(d,{AccessTokenMissingError:()=>m.AccessTokenMissingError,RefreshAccessTokenFailedError:()=>m.RefreshAccessTokenFailedError,getContext:()=>p.getContext,getVercelOidcToken:()=>f.getVercelOidcToken,getVercelOidcTokenSync:()=>f.getVercelOidcTokenSync,getVercelToken:()=>h.getVercelToken}),t.exports=l(d);var f=b(),p=y(),m=c(),h=u()})),S=x();const C=`${i}/`,w=new Set([`localhost`,`127.0.0.1`,`0.0.0.0`,`::1`,`[::1]`]);function T(e){return w.has(e.hostname)}const E=`x-vercel-protection-bypass`,D=`x-vercel-trusted-oidc-idp-token`;function O(e){return e.pathname.endsWith(`/ash/v1`)||e.pathname.includes(C)}async function k(e){let t=A(e),n=await M(t,e.resourceUrl);return n!==null&&j(t,n),t}function A(e){let t=new Headers(F(e.headers)),n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&O(e.resourceUrl)&&t.set(E,n),t}function j(e,t){e.has(`authorization`)||e.set(`authorization`,`Bearer ${t}`),e.set(D,t)}async function M(e,t){return N(t)?e.get(`x-vercel-oidc-token`)?.trim()||await P():null}function N(e){return!(!O(e)||T(e))}async function P(){let e=process.env.VERCEL_OIDC_TOKEN?.trim();try{let e=(await(0,S.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{return e??null}return e??null}function F(e){if(e!==void 0)return e instanceof Headers?e:Array.isArray(e)?e.map(([e,t])=>[e,t]):e}function I(){return{streamIndex:0}}function L(e){let t=B(e.events),n=e.session.streamIndex+e.events.length;return t?.type===`session.waiting`?{continuationToken:e.continuationToken??e.session.continuationToken,sessionId:e.sessionId,streamIndex:n}:I()}function R(e){let t;for(let n of e)V(n)&&(t=n.data.message??void 0);return t}function z(e){let t=B(e);return t?.type===`session.waiting`?`waiting`:t?.type===`session.failed`?`failed`:`completed`}function B(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(n!==void 0&&a(n))return n}}function V(e){return e.type===`message.completed`&&e.data.finishReason!==`tool-calls`}var H=class{continuationToken;sessionId;#e=!1;#t;constructor(e){this.continuationToken=e.continuationToken,this.sessionId=e.sessionId,this.#t=e.createStream}async result(){let e=[];for await(let t of this)e.push(t);return{events:e,message:R(e),sessionId:this.sessionId,status:z(e)}}[Symbol.asyncIterator](){if(this.#e)throw Error(`MessageResponse has already been consumed.`);return this.#e=!0,this.#t()}},U=class{#e;#t;constructor(e,t){this.#e=e,this.#t=t}get state(){return this.#t}async sendMessage(e,t){return this.send({message:e},t)}async send(e,t){let n=this.#t,{continuationToken:r,sessionId:i}=await this.#n(e,n,t);return new H({continuationToken:r,createStream:()=>this.#r(i,r,n,t),sessionId:i})}openStream(e){let t=this.#t.sessionId;if(!t)throw Error(`Session has no session ID. Send a message first.`);return v({host:this.#e.host,maxReconnectAttempts:this.#e.maxReconnectAttempts,resolveHeaders:()=>this.#e.resolveHeaders(),sessionId:t,signal:e?.signal,startIndex:e?.startIndex??this.#t.streamIndex})}async#n(e,t,i){let a=t.sessionId?n(t.sessionId):r,o=m(this.#e.host,a),s=await this.#e.resolveHeaders(i?.headers);s.set(`content-type`,`application/json`);let c=W({input:e,session:t});if(c===null)throw Error(`Session.send requires a non-empty message, inputResponses, or both.`);let l=await fetch(o,{body:JSON.stringify(c),headers:s,method:`POST`,signal:i?.signal??null});if(!l.ok){let e=await l.text();throw new d(l.status,e)}let u=await l.json(),f=(typeof u.sessionId==`string`?u.sessionId:void 0)??l.headers.get(`x-ash-session-id`)?.trim()??t.sessionId;if(!f)throw Error(`Message route did not return a session id.`);return{continuationToken:typeof u.continuationToken==`string`?u.continuationToken:void 0,sessionId:f}}async*#r(e,t,n,r){let i=[];try{let t=n.sessionId===e?n.streamIndex:0,o=this.#e.maxReconnectAttempts;for(;;){let n=await this.#i(e,t,r?.signal),s=!1;try{for await(let e of p(n))if(i.push(e),t+=1,yield e,a(e)){s=!0;break}}catch(e){if(!f(e))throw e}if(s||o<=0)break;--o}}finally{this.#t=L({continuationToken:t,events:i,sessionId:e,session:n})}}async#i(e,t,n){let r=m(this.#e.host,s(e),t>0?{startIndex:String(t)}:void 0),i=await this.#e.resolveHeaders(),a=await fetch(r,{headers:i,signal:n??null});if(!a.ok){let e=await a.text();throw new d(a.status,e)}if(!a.body)throw new d(a.status,`Response body is null.`);return a.body}};function W(e){let t={};return e.input.message!==void 0&&(t.message=e.input.message),e.input.inputResponses!==void 0&&e.input.inputResponses.length>0&&(t.inputResponses=e.input.inputResponses),e.session.continuationToken!==void 0&&(t.continuationToken=e.session.continuationToken),Object.keys(t).length===0||e.session.continuationToken===void 0&&t.message===void 0||`continuationToken`in t&&Object.keys(t).length===1?null:t}var G=class{#e;#t;#n;#r;constructor(e){this.#n=e.host,this.#e=e.auth,this.#t=e.headers,this.#r=e.maxReconnectAttempts??3}async health(){let e=m(this.#n,o),t=await this.#i(),n=await fetch(e,{headers:t});if(!n.ok){let e=await n.text();throw new d(n.status,e)}return await n.json()}session(e){let t;return t=typeof e==`string`?{continuationToken:e,streamIndex:0}:e||I(),new U({host:this.#n,maxReconnectAttempts:this.#r,resolveHeaders:e=>this.#i(e)},t)}async#i(e){let t=new Headers,n=await q(this.#t);for(let[e,r]of Object.entries(n))t.set(e,r);if(e)for(let[n,r]of Object.entries(e))t.set(n,r);let r=await this.#a();return r&&t.set(`authorization`,r),t}async#a(){let e=this.#e;if(e){if(`bearer`in e){let t=(await K(e.bearer)).trim();return t.length===0?void 0:`Bearer ${t}`}if(`basic`in e){let t=await K(e.basic.password);return`Basic ${J(e.basic.username,t)}`}}}};async function K(e){return typeof e==`function`?e():e}async function q(e){return e===void 0?{}:typeof e==`function`?await e():e}function J(e,t){let n=new TextEncoder().encode(`${e}:${t}`),r=Array.from(n,e=>String.fromCodePoint(e)).join(``);return btoa(r)}export{x as a,k as i,E as n,v as o,D as r,d as s,G as t};