experimental-ash 0.7.6 → 0.8.0

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 (78) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +79 -56
  3. package/dist/docs/public/channels/README.md +33 -23
  4. package/dist/docs/public/channels/attachments.md +42 -29
  5. package/dist/src/channel/adapter.d.ts +21 -28
  6. package/dist/src/channel/compiled-channel.d.ts +1 -1
  7. package/dist/src/channel/http.d.ts +29 -0
  8. package/dist/src/channel/http.js +30 -0
  9. package/dist/src/channel/schedule.d.ts +20 -0
  10. package/dist/src/channel/schedule.js +22 -1
  11. package/dist/src/channel/send.d.ts +1 -1
  12. package/dist/src/channel/send.js +22 -1
  13. package/dist/src/channel/types.d.ts +1 -1
  14. package/dist/src/chunks/{client-DBMG7iuf.js → client-BeZ_W7vl.js} +2 -2
  15. package/dist/src/chunks/{dev-authored-source-watcher-BcN7BUDE.js → dev-authored-source-watcher-BFC_yNcP.js} +1 -1
  16. package/dist/src/chunks/host-DMccRKcz.js +22 -0
  17. package/dist/src/chunks/{paths-BYIdCNw9.js → paths-B-aiDznc.js} +26 -26
  18. package/dist/src/chunks/{prewarm-DXhyk7i9.js → prewarm-CCbReSNm.js} +1 -1
  19. package/dist/src/chunks/types-MZUhN0Zy.js +1 -0
  20. package/dist/src/cli/commands/info.js +1 -1
  21. package/dist/src/cli/dev/repl.js +1 -1
  22. package/dist/src/cli/run.js +1 -1
  23. package/dist/src/compiled/.vendor-stamp.json +1 -1
  24. package/dist/src/compiled/@vercel/sandbox/index.d.ts +37 -3
  25. package/dist/src/evals/cli/eval.js +1 -1
  26. package/dist/src/execution/sandbox/bindings/local.d.ts +0 -2
  27. package/dist/src/execution/sandbox/bindings/local.js +1 -20
  28. package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
  29. package/dist/src/execution/sandbox/bindings/vercel.js +1 -12
  30. package/dist/src/harness/attachment-staging.js +54 -50
  31. package/dist/src/harness/emission.d.ts +14 -1
  32. package/dist/src/harness/emission.js +15 -2
  33. package/dist/src/harness/tool-loop.js +28 -2
  34. package/dist/src/internal/application/package.js +1 -1
  35. package/dist/src/internal/attachments/url-refs.d.ts +14 -0
  36. package/dist/src/internal/attachments/url-refs.js +20 -0
  37. package/dist/src/internal/nitro/host/configure-nitro-routes.d.ts +0 -1
  38. package/dist/src/internal/nitro/host/configure-nitro-routes.js +24 -17
  39. package/dist/src/internal/nitro/host/create-application-nitro.js +1 -16
  40. package/dist/src/internal/nitro/routes/agent-info/build-agent-info-response.d.ts +87 -0
  41. package/dist/src/internal/nitro/routes/{home-page/build-home-page-response.js → agent-info/build-agent-info-response.js} +6 -6
  42. package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.d.ts → agent-info/load-agent-info-data.d.ts} +8 -8
  43. package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.js → agent-info/load-agent-info-data.js} +7 -8
  44. package/dist/src/internal/nitro/routes/index.d.ts +10 -5
  45. package/dist/src/internal/nitro/routes/index.js +225 -18
  46. package/dist/src/internal/nitro/routes/info.d.ts +14 -0
  47. package/dist/src/internal/nitro/routes/info.js +50 -0
  48. package/dist/src/protocol/routes.d.ts +8 -6
  49. package/dist/src/protocol/routes.js +8 -6
  50. package/dist/src/public/channels/ash.js +1 -6
  51. package/dist/src/public/channels/index.d.ts +1 -1
  52. package/dist/src/public/channels/slack/attachments.d.ts +14 -18
  53. package/dist/src/public/channels/slack/attachments.js +30 -36
  54. package/dist/src/public/channels/slack/index.d.ts +0 -1
  55. package/dist/src/public/channels/slack/slackChannel.js +3 -3
  56. package/dist/src/public/definitions/defineChannel.d.ts +9 -7
  57. package/dist/src/public/definitions/defineChannel.js +5 -11
  58. package/dist/src/public/definitions/sandbox.d.ts +3 -3
  59. package/dist/src/public/sandbox/backends/vercel.d.ts +2 -2
  60. package/dist/src/public/sandbox/index.d.ts +2 -2
  61. package/dist/src/public/sandbox/vercel-sandbox.d.ts +11 -10
  62. package/dist/src/runtime/channels/registry.js +9 -3
  63. package/dist/src/runtime/resolve-channel.js +2 -1
  64. package/dist/src/shared/sandbox-backend.d.ts +4 -4
  65. package/dist/src/shared/sandbox-definition.d.ts +6 -36
  66. package/package.json +1 -1
  67. package/dist/src/chunks/host-33-Sb6vq.js +0 -22
  68. package/dist/src/chunks/types-D9Uv7nU4.js +0 -1
  69. package/dist/src/internal/nitro/host/load-home-page-web-assets.d.ts +0 -12
  70. package/dist/src/internal/nitro/host/load-home-page-web-assets.js +0 -34
  71. package/dist/src/internal/nitro/routes/home-page/build-home-page-response.d.ts +0 -87
  72. package/dist/src/internal/nitro/routes/home.d.ts +0 -6
  73. package/dist/src/internal/nitro/routes/home.js +0 -21
  74. package/dist/src/internal/nitro/routes/web-ui/assets/index-BQa8fbHJ.js +0 -11
  75. package/dist/src/internal/nitro/routes/web-ui/assets/style-Kqb6YxTP.css +0 -2
  76. package/dist/src/internal/nitro/routes/web-ui/index.html +0 -17
  77. package/dist/src/public/sandboxes/vercel-sandbox.d.ts +0 -41
  78. package/dist/src/public/sandboxes/vercel-sandbox.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a0710f7: feat(ash): update `SandboxSessionUseFn` to rely on backend-driven generic for its options instead of limiting to a central type
8
+
9
+ ### Patch Changes
10
+
11
+ - 4c6db10: fix(ash): fix request errors due to tool results missing in model messages after tool approval in previous turn
12
+
3
13
  ## 0.7.6
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -2,20 +2,9 @@
2
2
 
3
3
  Ash is a filesystem-first framework for durable backend agents on Vercel.
4
4
 
5
- You author an agent as a directory on disk. The directory is the contract:
5
+ You author an agent as a directory on disk. The directory is the contract — markdown for the parts a human should read like a spec, TypeScript for the parts that benefit from real types and runtime behavior.
6
6
 
7
- - `instructions.md` defines the always-on instructions prompt
8
- - `skills/` define optional procedures
9
- - `tools/` define typed executable integrations
10
- - `connections/` define external MCP server connections
11
- - `sandbox/` overrides the agent's single sandbox (optional) and seeds workspace files
12
- - `channels/` define message ingress and delivery
13
- - `subagents/` define specialist child agents
14
- - `schedules/` define recurring jobs
15
- - `lib/` holds shared authored code
16
- - `agent.ts` holds additive runtime config such as model, metadata, build, compaction, and workspace settings
17
-
18
- The framework package is `experimental-ash`. The CLI binary is `ash`.
7
+ The framework is called Ash. The published npm package is `experimental-ash`. The CLI binary is `ash`.
19
8
 
20
9
  ## What Ash Prioritizes
21
10
 
@@ -27,38 +16,53 @@ The framework package is `experimental-ash`. The CLI binary is `ash`.
27
16
  - A stable HTTP protocol with explicit `continuationToken` and `runId` contracts
28
17
  - A runtime model that keeps channels, harnesses, and workflow execution separate
29
18
 
30
- ## Current Mental Model
31
-
32
- Ash’s internal split is:
33
-
34
- - the channel normalizes inbound transport, applies auth and delivery policy, and owns `continuationToken`
35
- - the harness does one unit of AI work and returns `{ session, next }`
36
- - the runtime persists state, follows `next`, streams events, and owns workflow primitives
37
-
38
- That split is why the public HTTP protocol separates:
39
-
40
- - `continuationToken` for the next user message
41
- - `runId` for streaming and inspection
42
-
43
- ## Example Layout
19
+ ## Authored Directory
44
20
 
45
21
  ```text
46
22
  my-agent/
47
23
  ├── package.json
48
24
  ├── tsconfig.json
49
25
  └── agent/
50
- ├── agent.ts
51
- ├── instructions.md
52
- ├── skills/
53
- ├── tools/
54
- ├── connections/
55
- ├── sandbox/
56
- ├── channels/
57
- ├── subagents/
58
- ├── schedules/
59
- └── lib/
26
+ ├── agent.ts # additive runtime config (model, name, build, compaction, …)
27
+ ├── instructions.md # always-on instructions prompt
28
+ ├── tools/ # typed executable integrations
29
+ ├── skills/ # optional named procedures the model can load on demand
30
+ ├── hooks/ # lifecycle and stream-event subscribers
31
+ ├── channels/ # message ingress and delivery (HTTP, Slack, …)
32
+ ├── connections/ # external MCP server connections
33
+ ├── sandbox/ # the agent's single sandbox (optional override)
34
+ ├── workspace/ # files seeded into the sandbox on each session
35
+ ├── subagents/ # specialist child agents (reuse `defineAgent`)
36
+ ├── schedules/ # recurring jobs
37
+ └── lib/ # shared authored code imported by other files
60
38
  ```
61
39
 
40
+ ## Authoring Helpers
41
+
42
+ Every authored directory has a typed helper. Import each from the matching subpath:
43
+
44
+ | Helper | Subpath | Authored Location |
45
+ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------ |
46
+ | `defineAgent(...)` | `experimental-ash` | `agent.ts`, `subagents/<id>/agent.ts` |
47
+ | `defineInstructions(...)` | `experimental-ash/instructions` | `instructions.ts` (or `instructions.md`) |
48
+ | `defineTool(...)`, `defineBashTool(...)`, `defineReadFileTool(...)`, `defineWriteFileTool(...)`, `disableTool(...)` | `experimental-ash/tools` | `tools/<name>.ts` |
49
+ | `defineSkill(...)`, `getSkill(...)` | `experimental-ash/skills` | `skills/<name>.ts` (or `skills/<name>.md`) |
50
+ | `defineHook(...)` | `experimental-ash/hooks` | `hooks/<slug>.ts` |
51
+ | `defineChannel(...)`, `POST`, `GET` | `experimental-ash/channels` | `channels/<name>.ts` |
52
+ | `ashChannel(...)`, `slack`, `slackChannel(...)`, `vercelOidc(...)` | `experimental-ash/channels/ash`, `/slack`, `/auth` | reused from `channels/<name>.ts` |
53
+ | `defineSandbox(...)` | `experimental-ash/sandbox` | `sandbox.ts` (or `sandbox/sandbox.ts`) |
54
+ | `defineSchedule(...)` | `experimental-ash/schedules` | `schedules/<name>.ts` (or `schedules/<name>.md`) |
55
+ | `defineEvalSuite(...)` | `experimental-ash/evals` | `evals/<name>.eval.ts` |
56
+
57
+ Runtime accessors live on the subpath that owns the concern:
58
+
59
+ - `getSession()` — current session, turn, auth, parent lineage (`experimental-ash/context`)
60
+ - `getSandbox()` — live sandbox handle for the current agent (`experimental-ash/sandbox`)
61
+ - `getSkill(identifier)` — handle for a named skill visible to the current agent (`experimental-ash/skills`)
62
+ - `getContext(key)`, `requireContext(key)`, `hasContext(key)`, `setContext(key)`, `ensureContext(key, factory)` — unified context helpers (`experimental-ash/context`)
63
+
64
+ The complete API reference, including types and lower-level runtime primitives, is in [`./dist/docs/public/typescript-api.md`](./dist/docs/public/typescript-api.md).
65
+
62
66
  ## Tiny Example
63
67
 
64
68
  `agent/instructions.md`
@@ -95,7 +99,6 @@ import { defineAgent } from "experimental-ash";
95
99
 
96
100
  export default defineAgent({
97
101
  model: "openai/gpt-5.4-mini",
98
- name: "weather-agent",
99
102
  });
100
103
  ```
101
104
 
@@ -104,31 +107,51 @@ export default defineAgent({
104
107
  ```bash
105
108
  pnpm create experimental-ash-agent
106
109
  cd my-agent
107
- pnpm install
108
110
  pnpm dev
109
111
  ```
110
112
 
111
- To create an agent in the current empty directory, run `pnpm create experimental-ash-agent .`.
113
+ The wizard scaffolds the project, picks a model, and (for the REPL channel) installs dependencies and starts the dev server for you. To scaffold into the current empty directory, run `pnpm create experimental-ash-agent .`.
114
+
115
+ CLI commands:
116
+
117
+ - `ash info` — discovery results and compiled artifacts
118
+ - `ash build` — compile `.ash/` and build the host output
119
+ - `ash dev` — start the local runtime and REPL
120
+
121
+ ## Deploying
122
+
123
+ Ash is built for Vercel. The runtime is Nitro + Vercel Workflows. Read [`./dist/docs/public/vercel-deployment.md`](./dist/docs/public/vercel-deployment.md) for the deployment path, environment variables, and Vercel-specific configuration.
124
+
125
+ ## Read Next
126
+
127
+ These files ship inside the installed package at `node_modules/experimental-ash/dist/docs/public/`:
128
+
129
+ - [Full docs index](./dist/docs/public/README.md) — recommended entry point
130
+ - [Getting Started](./dist/docs/public/getting-started.md) — install, scaffold, and run locally
131
+ - [Project Layout](./dist/docs/public/project-layout.md) — every authored directory in depth
132
+ - [`agent.ts`](./dist/docs/public/agent-ts.md) — agent config reference
133
+ - [TypeScript API](./dist/docs/public/typescript-api.md) — complete `define*` and runtime helper reference
134
+ - [Vercel Deployment](./dist/docs/public/vercel-deployment.md) — deploy to production
135
+
136
+ By authoring concern: [Tools](./dist/docs/public/tools.md) · [Channels](./dist/docs/public/channels/README.md) · [Hooks](./dist/docs/public/hooks.md) · [Skills](./dist/docs/public/skills.md) · [Sandbox](./dist/docs/public/sandbox.md) · [Workspace](./dist/docs/public/workspace.md) · [Connections](./dist/docs/public/connections.md) · [Subagents](./dist/docs/public/subagents.md) · [Schedules](./dist/docs/public/schedules.md) · [Human In The Loop](./dist/docs/public/human-in-the-loop.md) · [Evals](./dist/docs/public/evals.md)
137
+
138
+ By runtime concern: [Sessions and Streaming](./dist/docs/public/runs-and-streaming.md) · [Session Context](./dist/docs/public/session-context.md) · [Context Control](./dist/docs/public/context-control.md) · [Auth and Route Protection](./dist/docs/public/auth-and-route-protection.md) · [CLI, Build, and Debugging](./dist/docs/public/cli-build-and-debugging.md) · [Instrumentation](./dist/docs/public/instrumentation.md)
139
+
140
+ ## Architecture (Internals)
112
141
 
113
- Useful commands:
142
+ You do not need this section to author an Ash agent — it documents the public HTTP protocol contracts so Ash composes predictably with other systems.
114
143
 
115
- - `ash info` shows discovery results and compiled artifacts
116
- - `ash build` compiles `.ash/` and builds the host output
117
- - `ash dev` starts the local runtime and REPL
144
+ Ash's internal split is:
118
145
 
119
- ## Public Docs
146
+ - the **channel** normalizes inbound transport, applies auth and delivery policy, and owns `continuationToken`
147
+ - the **harness** does one unit of AI work and returns `{ session, next }`
148
+ - the **runtime** persists state, follows `next`, streams events, and owns workflow primitives (`start()`, `resumeHook()`, `createHook()`, `getWritable()`)
120
149
 
121
- Start here:
150
+ That split is why the public HTTP protocol separates two distinct identifiers:
122
151
 
123
- 1. [`docs/public/README.md`](docs/public/README.md)
124
- 2. [`docs/public/getting-started.md`](docs/public/getting-started.md)
125
- 3. [`docs/public/project-layout.md`](docs/public/project-layout.md)
126
- 4. [`docs/public/agent-ts.md`](docs/public/agent-ts.md)
127
- 5. [`docs/public/typescript-api.md`](docs/public/typescript-api.md)
128
- 6. [`docs/public/connections.md`](docs/public/connections.md)
152
+ - `continuationToken` — channel-owned handle the caller uses to start the next user turn
153
+ - `runId` — runtime-owned handle for streaming and inspection
129
154
 
130
- ## Repo Guide
155
+ ## Changelog
131
156
 
132
- - [`packages/ash/README.md`](packages/ash/README.md) is the package-facing overview
133
- - [`skills/agent/SKILL.md`](skills/agent/SKILL.md) is the app-authoring skill
134
- - [`skills/framework/SKILL.md`](skills/framework/SKILL.md) is the internals skill
157
+ See [`./CHANGELOG.md`](./CHANGELOG.md) for the release history. The changelog ships inside the published package so agents can read it directly from `node_modules/experimental-ash/CHANGELOG.md` to evaluate upgrades.
@@ -217,42 +217,52 @@ and channel file.
217
217
  pass a `UserContent` array mixing text and file parts:
218
218
 
219
219
  ```ts
220
- POST("/message", async (req, { send }) => {
221
- const body = await req.json();
222
-
223
- await send(
224
- [
225
- { type: "text", text: body.message },
226
- { type: "file", data: body.fileData, mediaType: "image/png" },
227
- ],
228
- { auth: null, continuationToken: token },
229
- );
230
- });
220
+ await send(
221
+ [
222
+ { type: "text", text: body.message },
223
+ { type: "file", data: imageBytes, mediaType: "image/png" },
224
+ ],
225
+ { auth: null, continuationToken: token },
226
+ );
231
227
  ```
232
228
 
233
229
  For platforms like Slack where files are behind authenticated URLs,
234
- the channel fetches the bytes before calling `send()`:
230
+ put `URL` objects in `FilePart.data` and declare `fetchFile` on the
231
+ channel config:
235
232
 
236
233
  ```ts
237
- chat.onNewMention(async (thread, message) => {
238
- const parts = [{ type: "text", text: message.text }];
239
-
240
- for (const attachment of message.attachments) {
241
- const bytes = await fetchSlackFile(attachment.url, botToken);
242
- parts.push({ type: "file", data: bytes, mediaType: attachment.mediaType });
243
- }
234
+ defineChannel({
235
+ fetchFile(url) {
236
+ if (!url.startsWith("https://files.slack.com/")) return null;
237
+ return fetch(url, { headers: { authorization: `Bearer ${token}` } })
238
+ .then((r) => r.arrayBuffer())
239
+ .then((b) => ({ bytes: Buffer.from(b) }));
240
+ },
244
241
 
245
- await send(parts, { auth, continuationToken, state });
242
+ routes: [
243
+ POST("/webhook", async (req, { send }) => {
244
+ await send(
245
+ [
246
+ { type: "text", text: message.text },
247
+ ...message.attachments.map((a) => ({
248
+ type: "file" as const,
249
+ data: new URL(a.url),
250
+ mediaType: a.mediaType,
251
+ })),
252
+ ],
253
+ { auth, continuationToken, state },
254
+ );
255
+ }),
256
+ ],
246
257
  });
247
258
  ```
248
259
 
249
- No ref system or deferred resolution — the channel resolves bytes
250
- at send time.
260
+ See [Channel file uploads](./attachments.md) for the full guide.
251
261
 
252
262
  ## What To Read Next
253
263
 
254
264
  - [Slack channel setup](./slack.md)
255
- - [Channel attachments](./attachments.md)
265
+ - [Channel file uploads](./attachments.md)
256
266
  - [Project Layout](../project-layout.md)
257
267
  - [TypeScript API](../typescript-api.md)
258
268
  - [Auth And Route Protection](../auth-and-route-protection.md)
@@ -1,15 +1,19 @@
1
1
  ---
2
- title: "Channel attachments"
2
+ title: "Channel file uploads"
3
3
  description: "Deliver inbound file attachments to the agent."
4
4
  ---
5
5
 
6
- This guide explains how channels deliver inbound file attachments to the agent.
6
+ This guide explains how channels deliver inbound files to the agent.
7
7
 
8
8
  ## How it works
9
9
 
10
10
  `send()` accepts `string | UserContent`. To include file attachments,
11
11
  pass a `UserContent` array mixing text and file parts:
12
12
 
13
+ ### Inline bytes
14
+
15
+ Use for small files where bytes are available in the route handler:
16
+
13
17
  ```ts
14
18
  await send(
15
19
  [
@@ -20,39 +24,48 @@ await send(
20
24
  );
21
25
  ```
22
26
 
23
- The channel is responsible for resolving file bytes before calling `send`.
27
+ ### URL-based files with fetchFile
28
+
24
29
  For platforms like Slack where files are behind authenticated URLs,
25
- the channel fetches the bytes at send time:
30
+ put a `URL` object in `FilePart.data` and declare `fetchFile` on the
31
+ channel config:
26
32
 
27
33
  ```ts
28
- chat.onNewMention(async (thread, message) => {
29
- const parts = [{ type: "text", text: message.text }];
34
+ defineChannel({
35
+ fetchFile(url) {
36
+ if (!url.startsWith("https://files.slack.com/")) return null;
37
+ return fetch(url, { headers: { authorization: `Bearer ${token}` } })
38
+ .then((r) => r.arrayBuffer())
39
+ .then((b) => ({ bytes: Buffer.from(b) }));
40
+ },
30
41
 
31
- for (const attachment of message.attachments) {
32
- const bytes = await fetchSlackFile(attachment.url, botToken);
33
- parts.push({ type: "file", data: bytes, mediaType: attachment.mediaType });
34
- }
35
-
36
- await send(parts, { auth, continuationToken, state });
42
+ routes: [
43
+ POST("/webhook", async (req, { send }) => {
44
+ await send(
45
+ [
46
+ { type: "text", text: message.text },
47
+ ...message.attachments.map((a) => ({
48
+ type: "file" as const,
49
+ data: new URL(a.url),
50
+ mediaType: a.mediaType,
51
+ })),
52
+ ],
53
+ { auth, continuationToken, state },
54
+ );
55
+ }),
56
+ ],
37
57
  });
38
58
  ```
39
59
 
40
- ## Dropped attachments
41
-
42
- Attachments that can't be fetched (no URL, expired token, network failure)
43
- should be logged and dropped:
44
-
45
- ```ts
46
- if (attachment.url === undefined) {
47
- console.warn("[my-channel] dropped attachment — no url available", {
48
- name: attachment.name,
49
- });
50
- continue;
51
- }
52
- ```
60
+ The `URL` object survives the queue boundary as a string and is
61
+ reconstituted inside the workflow step. The staging pipeline calls
62
+ `fetchFile(url)` return bytes to stage the file to the sandbox,
63
+ or return `null` to let the URL pass through to the model provider.
53
64
 
54
- ## What you do NOT have to do
65
+ ## What the framework handles
55
66
 
56
- - Build refs or deferred resolution — bytes are resolved at send time.
57
- - Stream the bytes yourself the harness writes them to the sandbox.
58
- - Worry about step boundaries the bytes travel with the message payload.
67
+ - Staging bytes to the sandbox
68
+ - Enforcing upload policy (size and media-type limits)
69
+ - Hydrating files for the model call (inline bytes for images/PDFs, text
70
+ references for everything else)
71
+ - Reconstituting `URL` objects after queue serialization
@@ -1,7 +1,6 @@
1
1
  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
- import type { AttachmentRef } from "#internal/attachments/refs.js";
5
4
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
6
5
  import type { DeliverPayload } from "#channel/types.js";
7
6
  /**
@@ -54,43 +53,33 @@ export type ChannelEventHandlers<TCtx extends ChannelAdapterContext<any> = Chann
54
53
  [K in HandleMessageStreamEvent["type"]]?: EventHandler<K, TCtx>;
55
54
  };
56
55
  /**
57
- * Enriched resolver return shape. A resolver can return either a bare
58
- * {@link Buffer} or this record when it learns a more accurate
59
- * `mediaType` or `filename` mid-fetch (e.g. from an HTTP
60
- * `Content-Type` header or a storage-service listing).
56
+ * Enriched return shape from a channel's {@link ChannelAdapter.fetchFile}
57
+ * function. Return a bare {@link Buffer} when only bytes are known, or
58
+ * this record when the fetch discovers a more accurate `mediaType` or
59
+ * `filename` (e.g. from an HTTP `Content-Type` header).
61
60
  *
62
61
  * When fields are provided, staging prefers them over the values the
63
- * channel populated at ingestion time — the resolver is the source of
64
- * truth for bytes *and* for anything it discovers while fetching.
62
+ * channel populated at ingestion time.
65
63
  */
66
- export interface ResolvedAttachment {
64
+ export interface FetchFileResult {
67
65
  readonly bytes: Buffer;
68
66
  readonly mediaType?: string;
69
67
  readonly filename?: string;
70
68
  }
71
- /**
72
- * Channel-owned resolver that turns an opaque {@link AttachmentRef} back
73
- * into raw bytes.
74
- *
75
- * Channels that emit `ash-attachment:` refs provide this so staging can
76
- * re-fetch bytes inside a step boundary. Return a bare {@link Buffer} or
77
- * a {@link ResolvedAttachment} when fetch-time metadata should override
78
- * the original file part.
79
- */
80
- export interface AttachmentResolver<TParams = unknown, TCtx extends ChannelAdapterContext<any> = ChannelAdapterContext> {
81
- resolve(ref: AttachmentRef<TParams>, ctx: TCtx): Promise<Buffer | ResolvedAttachment>;
82
- }
83
69
  /**
84
70
  * Plain-object channel adapter with durable state, an optional inbound
85
71
  * delivery hook, event handlers, and optional attachment resolution.
86
72
  */
87
- export type ChannelAdapter<TCtx extends ChannelAdapterContext<any> = ChannelAdapterContext, TAttachmentParams = unknown> = {
73
+ export type ChannelAdapter<TCtx extends ChannelAdapterContext<any> = ChannelAdapterContext> = {
88
74
  /**
89
75
  * Stable durable identifier for serialization across step boundaries.
90
76
  * Must be unique across all adapters visible to one runtime bundle.
91
77
  *
92
- * Optional — defaults to `"http"` for the HTTP channel
93
- * or derived from the route file path.
78
+ * Optional — defaults to {@link HTTP_ADAPTER_KIND} (`"http"`) for the
79
+ * canonical session channel and every behaviorless authored channel,
80
+ * or is derived from the route file path as `channel:<name>` once
81
+ * `runtime/resolve-channel.ts` rewrites authored adapters that carry
82
+ * behavior.
94
83
  */
95
84
  readonly kind?: string;
96
85
  /**
@@ -121,13 +110,17 @@ export type ChannelAdapter<TCtx extends ChannelAdapterContext<any> = ChannelAdap
121
110
  */
122
111
  createAdapterContext?(base: ChannelAdapterContext<StateOf<TCtx>>): TCtx;
123
112
  /**
124
- * Optional resolver that turns an opaque {@link AttachmentRef}
125
- * produced by this adapter into raw bytes.
113
+ * Fetches bytes for a URL encountered in `FilePart.data`.
114
+ *
115
+ * Called by the staging pipeline when it encounters a `URL` object
116
+ * on a `FilePart`. Return `null` to let the URL pass through to the
117
+ * model provider (e.g. public images). Return bytes or
118
+ * {@link FetchFileResult} to stage the file to the sandbox.
126
119
  *
127
- * Required only for channels that put `ash-attachment:` URLs into
128
- * `FilePart.data`.
120
+ * Credentials should be captured in the closure at channel
121
+ * construction time.
129
122
  */
130
- readonly attachments?: AttachmentResolver<TAttachmentParams, TCtx>;
123
+ readonly fetchFile?: (url: string) => Promise<Buffer | FetchFileResult | null>;
131
124
  /**
132
125
  * Derives the continuation token from adapter state.
133
126
  *
@@ -10,7 +10,7 @@ export interface CompiledChannel<TState = undefined> {
10
10
  path: string;
11
11
  handler: RouteHandler<TState>;
12
12
  }[];
13
- readonly adapter: ChannelAdapter<any, any>;
13
+ readonly adapter: ChannelAdapter<any>;
14
14
  readonly receive?: (input: {
15
15
  readonly message: string;
16
16
  readonly args: Readonly<Record<string, unknown>>;
@@ -0,0 +1,29 @@
1
+ import type { ChannelAdapter } from "#channel/adapter.js";
2
+ /**
3
+ * Durable adapter kind for the canonical session channel and for every
4
+ * behaviorless user-authored channel returned by the `defineChannel`
5
+ * fast-path (no state, no `context()`, no event handlers, no `fetchFile`).
6
+ *
7
+ * The value is locked at `"http"` because it is persisted into durable
8
+ * workflow state under `serializedContext["ash.channel"].kind` and into
9
+ * sandbox telemetry tags as `channel: "http"`. Renaming the value would
10
+ * break rehydration for every in-flight session started under any prior
11
+ * build with "Unknown adapter kind: \"http\"". The `httpChannel` →
12
+ * `ashChannel` rename (commit `bd3d1b43`) intentionally renamed the
13
+ * channel identifier (file name, function name, framework constant) but
14
+ * deliberately left this adapter kind alone — the kind labels the
15
+ * adapter's transport class, not the channel's name, and the same slot
16
+ * is the rehydration target for every behaviorless authored channel
17
+ * regardless of which protocol it speaks.
18
+ */
19
+ export declare const HTTP_ADAPTER_KIND = "http";
20
+ /**
21
+ * Framework adapter installed for the canonical session channel and
22
+ * for every behaviorless user-authored channel.
23
+ *
24
+ * Carries no behavior — it is a bare discriminator that the runtime
25
+ * adapter registry uses to rehydrate `{ kind: "http" }` at every
26
+ * workflow step boundary. Registered in `FRAMEWORK_ADAPTERS`
27
+ * (`runtime/channels/registry.ts`).
28
+ */
29
+ export declare const HTTP_ADAPTER: ChannelAdapter;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Durable adapter kind for the canonical session channel and for every
3
+ * behaviorless user-authored channel returned by the `defineChannel`
4
+ * fast-path (no state, no `context()`, no event handlers, no `fetchFile`).
5
+ *
6
+ * The value is locked at `"http"` because it is persisted into durable
7
+ * workflow state under `serializedContext["ash.channel"].kind` and into
8
+ * sandbox telemetry tags as `channel: "http"`. Renaming the value would
9
+ * break rehydration for every in-flight session started under any prior
10
+ * build with "Unknown adapter kind: \"http\"". The `httpChannel` →
11
+ * `ashChannel` rename (commit `bd3d1b43`) intentionally renamed the
12
+ * channel identifier (file name, function name, framework constant) but
13
+ * deliberately left this adapter kind alone — the kind labels the
14
+ * adapter's transport class, not the channel's name, and the same slot
15
+ * is the rehydration target for every behaviorless authored channel
16
+ * regardless of which protocol it speaks.
17
+ */
18
+ export const HTTP_ADAPTER_KIND = "http";
19
+ /**
20
+ * Framework adapter installed for the canonical session channel and
21
+ * for every behaviorless user-authored channel.
22
+ *
23
+ * Carries no behavior — it is a bare discriminator that the runtime
24
+ * adapter registry uses to rehydrate `{ kind: "http" }` at every
25
+ * workflow step boundary. Registered in `FRAMEWORK_ADAPTERS`
26
+ * (`runtime/channels/registry.ts`).
27
+ */
28
+ export const HTTP_ADAPTER = {
29
+ kind: HTTP_ADAPTER_KIND,
30
+ };
@@ -1,6 +1,26 @@
1
+ import type { ChannelAdapter } from "#channel/adapter.js";
1
2
  import { type Session } from "#channel/session.js";
2
3
  import type { Runtime } from "#channel/types.js";
3
4
  import type { ResolvedChannelDefinition } from "#runtime/types.js";
5
+ /**
6
+ * Durable adapter kind used when a schedule triggers a session that does
7
+ * not target a channel.
8
+ *
9
+ * Framework-owned — authored code never constructs a schedule adapter
10
+ * directly. Emitted by {@link ScheduleDispatcher.trigger} when the
11
+ * authored schedule has no `channel` field (the only path available to
12
+ * markdown schedules, which are forbidden from declaring a channel).
13
+ */
14
+ export declare const SCHEDULE_ADAPTER_KIND = "schedule";
15
+ /**
16
+ * Framework adapter installed for channel-less schedules.
17
+ *
18
+ * Carries no behavior — it is a bare discriminator so the runtime adapter
19
+ * registry can rehydrate `{ kind: "schedule" }` at every workflow step
20
+ * boundary. Registered in `FRAMEWORK_ADAPTERS`
21
+ * (`runtime/channels/registry.ts`).
22
+ */
23
+ export declare const SCHEDULE_ADAPTER: ChannelAdapter;
4
24
  type ScheduleChannel = Pick<ResolvedChannelDefinition, "receive" | "adapter">;
5
25
  export interface ScheduleTriggerInput {
6
26
  readonly channel?: {
@@ -6,6 +6,27 @@ const APP_AUTH = {
6
6
  principalId: "ash:app",
7
7
  principalType: "runtime",
8
8
  };
9
+ /**
10
+ * Durable adapter kind used when a schedule triggers a session that does
11
+ * not target a channel.
12
+ *
13
+ * Framework-owned — authored code never constructs a schedule adapter
14
+ * directly. Emitted by {@link ScheduleDispatcher.trigger} when the
15
+ * authored schedule has no `channel` field (the only path available to
16
+ * markdown schedules, which are forbidden from declaring a channel).
17
+ */
18
+ export const SCHEDULE_ADAPTER_KIND = "schedule";
19
+ /**
20
+ * Framework adapter installed for channel-less schedules.
21
+ *
22
+ * Carries no behavior — it is a bare discriminator so the runtime adapter
23
+ * registry can rehydrate `{ kind: "schedule" }` at every workflow step
24
+ * boundary. Registered in `FRAMEWORK_ADAPTERS`
25
+ * (`runtime/channels/registry.ts`).
26
+ */
27
+ export const SCHEDULE_ADAPTER = {
28
+ kind: SCHEDULE_ADAPTER_KIND,
29
+ };
9
30
  /**
10
31
  * Dispatcher for scheduled task execution.
11
32
  *
@@ -27,7 +48,7 @@ export class ScheduleDispatcher {
27
48
  async trigger(input) {
28
49
  if (input.channel === undefined) {
29
50
  const handle = await this.runtime.run({
30
- adapter: { kind: "schedule" },
51
+ adapter: SCHEDULE_ADAPTER,
31
52
  auth: APP_AUTH,
32
53
  input: { message: input.markdown },
33
54
  mode: "task",
@@ -1,4 +1,4 @@
1
1
  import type { ChannelAdapter } from "#channel/adapter.js";
2
2
  import type { Runtime } from "#channel/types.js";
3
3
  import type { SendFn } from "#channel/routes.js";
4
- export declare function createSendFn<TState = undefined>(runtime: Runtime, adapter: ChannelAdapter<any, any>, channelName: string): SendFn<TState>;
4
+ export declare function createSendFn<TState = undefined>(runtime: Runtime, adapter: ChannelAdapter<any>, channelName: string): SendFn<TState>;
@@ -1,4 +1,5 @@
1
1
  import { createSession } from "#channel/session.js";
2
+ import { serializeUrlFilePart } from "#internal/attachments/url-refs.js";
2
3
  import { createLogger } from "#internal/logging.js";
3
4
  const log = createLogger("channel.send");
4
5
  export function createSendFn(runtime, adapter, channelName) {
@@ -7,7 +8,8 @@ export function createSendFn(runtime, adapter, channelName) {
7
8
  const rawToken = options.continuationToken;
8
9
  const continuationToken = `${channelName}:${rawToken}`;
9
10
  const state = options.state;
10
- const { message, inputResponses } = normalizeSendInput(input);
11
+ const { message: rawMessage, inputResponses } = normalizeSendInput(input);
12
+ const message = serializeUrlFilePartsInMessage(rawMessage);
11
13
  try {
12
14
  const { sessionId } = await runtime.deliver({
13
15
  auth,
@@ -36,6 +38,25 @@ export function createSendFn(runtime, adapter, channelName) {
36
38
  return createSession(handle.sessionId, rawToken, runtime);
37
39
  };
38
40
  }
41
+ /**
42
+ * Serializes `URL` objects in `FilePart.data` to `ash-url:` strings
43
+ * before the message crosses the queue boundary. The staging pipeline
44
+ * reconstitutes them on the other side.
45
+ */
46
+ function serializeUrlFilePartsInMessage(message) {
47
+ if (message === undefined || typeof message === "string") {
48
+ return message;
49
+ }
50
+ let changed = false;
51
+ const result = message.map((part) => {
52
+ if (part.type === "file" && part.data instanceof URL && part.data.protocol !== "data:") {
53
+ changed = true;
54
+ return { ...part, data: serializeUrlFilePart(part.data) };
55
+ }
56
+ return part;
57
+ });
58
+ return changed ? result : message;
59
+ }
39
60
  function normalizeSendInput(input) {
40
61
  if (typeof input === "string") {
41
62
  return { message: input };