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.
- package/CHANGELOG.md +43 -2
- package/dist/docs/public/channels/README.md +75 -9
- package/dist/docs/public/schedules.md +13 -4
- package/dist/src/channel/adapter-context.d.ts +4 -0
- package/dist/src/channel/adapter-context.js +6 -0
- package/dist/src/channel/adapter.d.ts +10 -10
- package/dist/src/channel/cross-channel-receive.d.ts +1 -1
- package/dist/src/channel/cross-channel-receive.js +40 -0
- package/dist/src/channel/routes.d.ts +7 -0
- package/dist/src/channel/send.js +1 -1
- package/dist/src/channel/session.d.ts +47 -1
- package/dist/src/channel/session.js +46 -0
- package/dist/src/channel/types.d.ts +6 -5
- package/dist/src/chunks/client-CKsU8Li3.js +4 -0
- package/dist/src/chunks/{dev-authored-source-watcher-CG6kri3T.js → dev-authored-source-watcher-j7YWh2Gx.js} +1 -1
- package/dist/src/chunks/{host-CIU0NATc.js → host-DkTSR6YJ.js} +2 -2
- package/dist/src/chunks/{paths-CvbqpwTh.js → paths-Dwv0Eash.js} +22 -22
- package/dist/src/chunks/{prewarm-C_Vd0JR7.js → prewarm-CQYfka30.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/client/client.js +2 -1
- package/dist/src/client/index.d.ts +3 -0
- package/dist/src/client/index.js +1 -0
- package/dist/src/client/message-reducer-types.d.ts +130 -0
- package/dist/src/client/message-reducer-types.js +1 -0
- package/dist/src/client/message-reducer.d.ts +14 -0
- package/dist/src/client/message-reducer.js +462 -0
- package/dist/src/client/open-stream.js +2 -4
- package/dist/src/client/reducer.d.ts +63 -0
- package/dist/src/client/reducer.js +1 -0
- package/dist/src/client/session.js +3 -5
- package/dist/src/client/url.d.ts +8 -0
- package/dist/src/client/url.js +34 -0
- package/dist/src/compiler/module-map.js +12 -0
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -34
- package/dist/src/execution/workflow-entry.js +35 -31
- package/dist/src/execution/workflow-steps.d.ts +16 -0
- package/dist/src/execution/workflow-steps.js +32 -4
- package/dist/src/harness/attachment-staging.js +2 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +13 -8
- package/dist/src/public/channels/slack/api.js +31 -17
- package/dist/src/public/channels/slack/index.d.ts +2 -2
- package/dist/src/public/channels/slack/index.js +1 -0
- package/dist/src/public/channels/slack/interactions.js +3 -3
- package/dist/src/public/channels/slack/slackChannel.d.ts +5 -3
- package/dist/src/public/channels/slack/slackChannel.js +26 -15
- package/dist/src/public/channels/twilio/api.d.ts +9 -0
- package/dist/src/public/channels/twilio/api.js +11 -0
- package/dist/src/public/channels/twilio/index.d.ts +1 -1
- package/dist/src/public/channels/twilio/index.js +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
- package/dist/src/public/channels/twilio/twilioChannel.js +8 -11
- package/dist/src/public/definitions/defineChannel.d.ts +9 -1
- package/dist/src/public/definitions/defineChannel.js +7 -11
- package/dist/src/public/definitions/sandbox.d.ts +2 -3
- package/dist/src/public/sandbox/backends/vercel.d.ts +4 -4
- package/dist/src/public/sandbox/backends/vercel.js +2 -2
- package/dist/src/public/sandbox/index.d.ts +1 -1
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +3 -28
- package/dist/src/react/index.d.ts +3 -0
- package/dist/src/react/index.js +3 -0
- package/dist/src/react/use-ash-agent.d.ts +79 -0
- package/dist/src/react/use-ash-agent.js +330 -0
- package/dist/src/runtime/types.d.ts +1 -2
- package/dist/src/shared/sandbox-backend.d.ts +4 -4
- package/dist/src/shared/sandbox-definition.d.ts +6 -6
- package/package.json +15 -2
- 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 `
|
|
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 `
|
|
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
|
|
301
|
-
|
|
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
|
-
|
|
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 `
|
|
320
|
-
thread, or `
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
####
|
|
49
|
+
#### Slack thread anchoring
|
|
50
50
|
|
|
51
|
-
A
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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;
|
package/dist/src/channel/send.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
164
|
-
*
|
|
165
|
-
* `
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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};
|