experimental-ash 0.7.5 → 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.
- package/CHANGELOG.md +16 -0
- package/README.md +79 -56
- package/dist/docs/public/channels/README.md +33 -23
- package/dist/docs/public/channels/attachments.md +42 -29
- package/dist/src/channel/adapter.d.ts +21 -28
- package/dist/src/channel/compiled-channel.d.ts +1 -1
- package/dist/src/channel/http.d.ts +29 -0
- package/dist/src/channel/http.js +30 -0
- package/dist/src/channel/schedule.d.ts +20 -0
- package/dist/src/channel/schedule.js +22 -1
- package/dist/src/channel/send.d.ts +1 -1
- package/dist/src/channel/send.js +22 -1
- package/dist/src/channel/types.d.ts +1 -1
- package/dist/src/chunks/{client-DBMG7iuf.js → client-BeZ_W7vl.js} +2 -2
- package/dist/src/chunks/{dev-authored-source-watcher-BcN7BUDE.js → dev-authored-source-watcher-BFC_yNcP.js} +1 -1
- package/dist/src/chunks/host-DMccRKcz.js +22 -0
- package/dist/src/chunks/{paths-BYIdCNw9.js → paths-B-aiDznc.js} +26 -26
- package/dist/src/chunks/{prewarm-DXhyk7i9.js → prewarm-CCbReSNm.js} +1 -1
- package/dist/src/chunks/types-MZUhN0Zy.js +1 -0
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/dev/environment.d.ts +3 -2
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +37 -3
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/local.d.ts +0 -2
- package/dist/src/execution/sandbox/bindings/local.js +1 -20
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -12
- package/dist/src/harness/attachment-staging.js +54 -50
- package/dist/src/harness/emission.d.ts +14 -1
- package/dist/src/harness/emission.js +15 -2
- package/dist/src/harness/tool-loop.js +58 -15
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/attachments/url-refs.d.ts +14 -0
- package/dist/src/internal/attachments/url-refs.js +20 -0
- package/dist/src/internal/nitro/host/configure-nitro-routes.d.ts +0 -1
- package/dist/src/internal/nitro/host/configure-nitro-routes.js +24 -17
- package/dist/src/internal/nitro/host/create-application-nitro.js +1 -16
- package/dist/src/internal/nitro/routes/agent-info/build-agent-info-response.d.ts +87 -0
- package/dist/src/internal/nitro/routes/{home-page/build-home-page-response.js → agent-info/build-agent-info-response.js} +6 -6
- package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.d.ts → agent-info/load-agent-info-data.d.ts} +8 -8
- package/dist/src/internal/nitro/routes/{home-page/load-home-page-data.js → agent-info/load-agent-info-data.js} +7 -8
- package/dist/src/internal/nitro/routes/index.d.ts +10 -5
- package/dist/src/internal/nitro/routes/index.js +225 -18
- package/dist/src/internal/nitro/routes/info.d.ts +14 -0
- package/dist/src/internal/nitro/routes/info.js +50 -0
- package/dist/src/protocol/routes.d.ts +8 -6
- package/dist/src/protocol/routes.js +8 -6
- package/dist/src/public/channels/ash.js +1 -6
- package/dist/src/public/channels/index.d.ts +1 -1
- package/dist/src/public/channels/slack/attachments.d.ts +14 -18
- package/dist/src/public/channels/slack/attachments.js +30 -36
- package/dist/src/public/channels/slack/index.d.ts +0 -1
- package/dist/src/public/channels/slack/slackChannel.js +3 -3
- package/dist/src/public/definitions/defineChannel.d.ts +9 -7
- package/dist/src/public/definitions/defineChannel.js +5 -11
- package/dist/src/public/definitions/sandbox.d.ts +3 -3
- package/dist/src/public/sandbox/backends/vercel.d.ts +2 -2
- package/dist/src/public/sandbox/index.d.ts +2 -2
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +11 -10
- package/dist/src/runtime/channels/registry.js +9 -3
- package/dist/src/runtime/resolve-channel.js +2 -1
- package/dist/src/shared/sandbox-backend.d.ts +4 -4
- package/dist/src/shared/sandbox-definition.d.ts +6 -36
- package/package.json +1 -1
- package/dist/src/chunks/host-33-Sb6vq.js +0 -22
- package/dist/src/chunks/types-D9Uv7nU4.js +0 -1
- package/dist/src/internal/nitro/host/load-home-page-web-assets.d.ts +0 -12
- package/dist/src/internal/nitro/host/load-home-page-web-assets.js +0 -34
- package/dist/src/internal/nitro/routes/home-page/build-home-page-response.d.ts +0 -87
- package/dist/src/internal/nitro/routes/home.d.ts +0 -6
- package/dist/src/internal/nitro/routes/home.js +0 -21
- package/dist/src/internal/nitro/routes/web-ui/assets/index-BQa8fbHJ.js +0 -11
- package/dist/src/internal/nitro/routes/web-ui/assets/style-Kqb6YxTP.css +0 -2
- package/dist/src/internal/nitro/routes/web-ui/index.html +0 -17
- package/dist/src/public/sandboxes/vercel-sandbox.d.ts +0 -41
- package/dist/src/public/sandboxes/vercel-sandbox.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.7.6
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 9b048ce: fix(ash): pass system messages to `agent.stream` under `instructions` to prevent AI SDK error
|
|
18
|
+
|
|
3
19
|
## 0.7.5
|
|
4
20
|
|
|
5
21
|
### 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
|
-
|
|
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
|
-
##
|
|
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
|
-
├──
|
|
53
|
-
├──
|
|
54
|
-
├──
|
|
55
|
-
├──
|
|
56
|
-
├──
|
|
57
|
-
├──
|
|
58
|
-
├──
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
That split is why the public HTTP protocol separates two distinct identifiers:
|
|
122
151
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
##
|
|
155
|
+
## Changelog
|
|
131
156
|
|
|
132
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
+
put `URL` objects in `FilePart.data` and declare `fetchFile` on the
|
|
231
|
+
channel config:
|
|
235
232
|
|
|
236
233
|
```ts
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
27
|
+
### URL-based files with fetchFile
|
|
28
|
+
|
|
24
29
|
For platforms like Slack where files are behind authenticated URLs,
|
|
25
|
-
|
|
30
|
+
put a `URL` object in `FilePart.data` and declare `fetchFile` on the
|
|
31
|
+
channel config:
|
|
26
32
|
|
|
27
33
|
```ts
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
65
|
+
## What the framework handles
|
|
55
66
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
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
|
|
58
|
-
* {@link Buffer}
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
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
|
|
64
|
-
* truth for bytes *and* for anything it discovers while fetching.
|
|
62
|
+
* channel populated at ingestion time.
|
|
65
63
|
*/
|
|
66
|
-
export interface
|
|
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
|
|
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
|
|
93
|
-
*
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
-
*
|
|
128
|
-
*
|
|
120
|
+
* Credentials should be captured in the closure at channel
|
|
121
|
+
* construction time.
|
|
129
122
|
*/
|
|
130
|
-
readonly
|
|
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
|
|
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:
|
|
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
|
|
4
|
+
export declare function createSendFn<TState = undefined>(runtime: Runtime, adapter: ChannelAdapter<any>, channelName: string): SendFn<TState>;
|
package/dist/src/channel/send.js
CHANGED
|
@@ -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 };
|