experimental-ash 0.18.3 → 0.20.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 +35 -0
- package/dist/docs/internals/context.md +7 -0
- package/dist/docs/public/channels/README.md +5 -0
- package/dist/docs/public/channels/slack.md +58 -4
- package/dist/docs/public/hooks.md +4 -2
- package/dist/docs/public/sandbox.md +71 -49
- package/dist/docs/public/tools.md +55 -3
- package/dist/docs/public/typescript-api.md +6 -1
- package/dist/src/channel/adapter.js +12 -2
- package/dist/src/channel/routes.d.ts +9 -1
- package/dist/src/channel/send.js +3 -3
- package/dist/src/channel/types.d.ts +3 -1
- package/dist/src/chunks/{dev-authored-source-watcher-j7YWh2Gx.js → dev-authored-source-watcher-NA8BZIXr.js} +1 -1
- package/dist/src/chunks/host-DOH_6p0f.js +22 -0
- package/dist/src/chunks/{paths-Dwv0Eash.js → paths-CbL9m08U.js} +25 -25
- package/dist/src/chunks/{prewarm-CQYfka30.js → prewarm-Bz6Jr9J1.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +7 -7
- package/dist/src/compiled/@ai-sdk/anthropic/index.js +2 -2
- package/dist/src/compiled/@ai-sdk/anthropic/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/google/index.js +6 -6
- package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/index.js +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/openai/index.js +6 -10
- package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/otel/index.js +2 -2
- package/dist/src/compiled/@ai-sdk/otel/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/provider/package.json +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +6 -1
- package/dist/src/compiled/@workflow/core/index.js +1 -1
- package/dist/src/compiled/@workflow/core/runtime.js +5 -5
- package/dist/src/compiled/@workflow/core/workflow.js +1 -1
- package/dist/src/compiled/@workflow/errors/index.js +1 -1
- package/dist/src/compiled/_chunks/workflow/{context-errors-CmtmBosi.js → context-errors-zbKocOyk.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/dist-CpUQh3NH.js +14 -0
- package/dist/src/compiled/_chunks/workflow/{resume-hook-BqY8TqOE.js → resume-hook-CL8Ed91K.js} +2 -2
- package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +1 -0
- package/dist/src/context/hook-lifecycle.js +5 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/node-step.js +1 -0
- package/dist/src/execution/sandbox/bash-tool.js +1 -1
- package/dist/src/execution/sandbox/bindings/local.d.ts +14 -1
- package/dist/src/execution/sandbox/bindings/local.js +22 -16
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +6 -0
- package/dist/src/execution/sandbox/bindings/vercel.js +25 -14
- package/dist/src/execution/sandbox/glob-tool.js +1 -1
- package/dist/src/execution/sandbox/grep-tool.js +1 -1
- package/dist/src/execution/sandbox/lazy-backend.d.ts +15 -0
- package/dist/src/execution/sandbox/lazy-backend.js +33 -0
- package/dist/src/execution/sandbox/read-file-tool.js +1 -1
- package/dist/src/execution/sandbox/ripgrep-probe.js +1 -1
- package/dist/src/execution/sandbox/session.js +49 -31
- package/dist/src/execution/sandbox/stream-utils.d.ts +2 -2
- package/dist/src/execution/sandbox/stream-utils.js +1 -1
- package/dist/src/execution/sandbox/write-file-tool.js +3 -3
- package/dist/src/execution/skills/types.d.ts +1 -1
- package/dist/src/execution/workflow-entry.d.ts +2 -4
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/harness/attachment-staging.js +10 -11
- package/dist/src/harness/execute-tool.d.ts +1 -0
- package/dist/src/harness/messages.js +15 -0
- package/dist/src/harness/tools.js +5 -0
- package/dist/src/harness/types.d.ts +6 -7
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/sandbox.d.ts +8 -2
- package/dist/src/internal/authored-definition/sandbox.js +10 -2
- package/dist/src/internal/authored-definition/schema-backed.js +12 -1
- package/dist/src/internal/nitro/host/configure-nitro-routes.js +1 -1
- package/dist/src/internal/workflow-bundle/builder-support.d.ts +1 -0
- package/dist/src/internal/workflow-bundle/builder-support.js +14 -0
- package/dist/src/internal/workflow-bundle/builder.js +6 -6
- package/dist/src/public/channels/slack/api-encoding.d.ts +1 -1
- package/dist/src/public/channels/slack/api-encoding.js +1 -7
- package/dist/src/public/channels/slack/api.d.ts +5 -4
- package/dist/src/public/channels/slack/api.js +6 -6
- package/dist/src/public/channels/slack/index.d.ts +3 -0
- package/dist/src/public/channels/slack/index.js +2 -0
- package/dist/src/public/channels/slack/slackChannel.d.ts +12 -4
- package/dist/src/public/channels/slack/slackChannel.js +4 -1
- package/dist/src/public/channels/slack/thread.d.ts +26 -0
- package/dist/src/public/channels/slack/thread.js +45 -0
- package/dist/src/public/definitions/sandbox.d.ts +1 -1
- package/dist/src/public/definitions/tool.d.ts +25 -0
- package/dist/src/public/sandbox/backends/default.d.ts +16 -1
- package/dist/src/public/sandbox/backends/default.js +7 -19
- package/dist/src/public/sandbox/backends/local.d.ts +7 -4
- package/dist/src/public/sandbox/backends/local.js +7 -5
- package/dist/src/public/sandbox/backends/vercel.d.ts +9 -3
- package/dist/src/public/sandbox/backends/vercel.js +9 -3
- package/dist/src/public/sandbox/index.d.ts +3 -2
- package/dist/src/public/sandbox/local-sandbox.d.ts +7 -0
- package/dist/src/public/sandbox/local-sandbox.js +1 -0
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +13 -1
- package/dist/src/public/tools/index.d.ts +1 -1
- package/dist/src/runtime/resolve-sandbox.js +5 -1
- package/dist/src/runtime/resolve-tool.js +3 -0
- package/dist/src/runtime/skills/sandbox-access.js +3 -3
- package/dist/src/runtime/types.d.ts +17 -2
- package/dist/src/shared/sandbox-definition.d.ts +16 -1
- package/dist/src/shared/sandbox-session.d.ts +53 -121
- package/package.json +9 -9
- package/dist/src/chunks/host-C19hLVqS.js +0 -22
- package/dist/src/compiled/_chunks/workflow/dist-4zn5tehu.js +0 -10
- package/dist/src/compiled/_chunks/workflow/dist-DTWUhyDN.js +0 -5
- package/dist/src/compiled/_chunks/workflow/sleep-D30F1GSr.js +0 -1
- /package/dist/src/compiled/_chunks/workflow/{dist-B6aByiku.js → dist-0iNBqPYp.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{dist-CVo7knbW.js → dist-D774SUM4.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{src-Bc9OYRaN.js → src-ClRYdO4-.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{symbols-DkV1V0kM.js → symbols-D-4tVV8x.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-Cq5QjRq8.js → token-CsNmv7KW.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-Duaoxfi5.js → token-j5Cl4rrs.js} +0 -0
|
@@ -4,11 +4,17 @@ import { buildSandboxSession } from "#execution/sandbox/session.js";
|
|
|
4
4
|
import { streamToBuffer } from "#execution/sandbox/stream-utils.js";
|
|
5
5
|
/**
|
|
6
6
|
* Creates the Vercel-backed sandbox backend.
|
|
7
|
+
*
|
|
8
|
+
* Any author-supplied `createOptions` are forwarded to the Vercel SDK
|
|
9
|
+
* `Sandbox.create(...)` for every fresh sandbox the framework creates
|
|
10
|
+
* (template at prewarm time, session at first-time session-create). On
|
|
11
|
+
* resume (`Sandbox.get`) no create happens, so they are not re-applied.
|
|
7
12
|
*/
|
|
8
13
|
export function createVercelSandboxBackend(input = {}) {
|
|
9
14
|
const loadSandboxModule = input.loadSandboxModule ?? (async () => await import("#compiled/@vercel/sandbox/index.js"));
|
|
10
15
|
const createOptions = {
|
|
11
16
|
timeout: DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
17
|
+
...input.createOptions,
|
|
12
18
|
};
|
|
13
19
|
const prewarmedTemplates = new Map();
|
|
14
20
|
return {
|
|
@@ -130,10 +136,10 @@ async function ensureTemplate(input) {
|
|
|
130
136
|
}
|
|
131
137
|
for (const file of input.seedFiles) {
|
|
132
138
|
if (typeof file.content === "string") {
|
|
133
|
-
await templateSession.writeTextFile(file.
|
|
139
|
+
await templateSession.writeTextFile({ content: file.content, path: file.path });
|
|
134
140
|
}
|
|
135
141
|
else {
|
|
136
|
-
await templateSession.writeBinaryFile(file.
|
|
142
|
+
await templateSession.writeBinaryFile({ content: file.content, path: file.path });
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
145
|
const snapshot = await sandbox.snapshot();
|
|
@@ -150,8 +156,13 @@ async function ensureSession(input) {
|
|
|
150
156
|
await ensureVercelSandboxTags(existing, input.tags);
|
|
151
157
|
return existing;
|
|
152
158
|
}
|
|
159
|
+
// The Vercel SDK rejects `runtime` when `source` is a snapshot — the
|
|
160
|
+
// runtime is already baked into the snapshot's filesystem. Strip it
|
|
161
|
+
// from the consumer-supplied create options for the session path
|
|
162
|
+
// only; template prewarm still honors `runtime`.
|
|
163
|
+
const { runtime: _runtime, ...sessionCreateOptions } = input.createOptions;
|
|
153
164
|
const createParams = {
|
|
154
|
-
...
|
|
165
|
+
...sessionCreateOptions,
|
|
155
166
|
name: sandboxName,
|
|
156
167
|
persistent: true,
|
|
157
168
|
source: { snapshotId: input.snapshotId, type: "snapshot" },
|
|
@@ -183,24 +194,24 @@ function createHandle(sandbox, sessionKey) {
|
|
|
183
194
|
function createVercelInternalSandboxSession(sandbox, id) {
|
|
184
195
|
return {
|
|
185
196
|
id,
|
|
186
|
-
async readFile(resolvedPath) {
|
|
187
|
-
const stream = await sandbox.readFile({ path: resolvedPath });
|
|
188
|
-
return stream ?? null;
|
|
189
|
-
},
|
|
190
197
|
resolvePath: resolveVercelSandboxPath,
|
|
191
|
-
async runCommand(
|
|
198
|
+
async runCommand(options) {
|
|
192
199
|
const result = await sandbox.runCommand({
|
|
193
|
-
args: ["-lc", command],
|
|
200
|
+
args: ["-lc", options.command],
|
|
194
201
|
cmd: "bash",
|
|
195
|
-
cwd: options
|
|
196
|
-
signal: options
|
|
202
|
+
cwd: options.workingDirectory ?? WORKSPACE_ROOT,
|
|
203
|
+
signal: options.abortSignal,
|
|
197
204
|
});
|
|
198
205
|
const [stdout, stderr] = await Promise.all([result.stdout(), result.stderr()]);
|
|
199
206
|
return { exitCode: result.exitCode, stderr, stdout };
|
|
200
207
|
},
|
|
201
|
-
async
|
|
202
|
-
const
|
|
203
|
-
|
|
208
|
+
async readFile(options) {
|
|
209
|
+
const stream = await sandbox.readFile({ path: options.path });
|
|
210
|
+
return stream ?? null;
|
|
211
|
+
},
|
|
212
|
+
async writeFile(options) {
|
|
213
|
+
const bytes = await streamToBuffer(options.content);
|
|
214
|
+
await sandbox.writeFiles([{ content: bytes, path: options.path }]);
|
|
204
215
|
},
|
|
205
216
|
};
|
|
206
217
|
}
|
|
@@ -24,7 +24,7 @@ export async function executeGlobOnSandbox(args) {
|
|
|
24
24
|
const command = (await ripgrepIsAvailable(sandbox))
|
|
25
25
|
? buildRipgrepCommand({ normalizedPath, pattern: args.pattern })
|
|
26
26
|
: buildPosixFindCommand({ normalizedPath, pattern: args.pattern });
|
|
27
|
-
const result = await sandbox.runCommand(command);
|
|
27
|
+
const result = await sandbox.runCommand({ command });
|
|
28
28
|
// Both ripgrep and POSIX find use conventional exit code semantics:
|
|
29
29
|
// 0 — operation succeeded (results may be empty for find)
|
|
30
30
|
// 1 — ripgrep-specific: no matches found (legitimate empty result)
|
|
@@ -41,7 +41,7 @@ export async function executeGrepOnSandbox(args) {
|
|
|
41
41
|
normalizedPath,
|
|
42
42
|
pattern: args.pattern,
|
|
43
43
|
});
|
|
44
|
-
const result = await sandbox.runCommand(command);
|
|
44
|
+
const result = await sandbox.runCommand({ command });
|
|
45
45
|
// Both ripgrep and POSIX grep use the same conventional exit codes:
|
|
46
46
|
// 0 — one or more matches were found
|
|
47
47
|
// 1 — no matches found (legitimate empty result)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a backend-producing function in a `SandboxBackend` proxy that
|
|
4
|
+
* invokes the function exactly once, on first access to any of `.name`,
|
|
5
|
+
* `.create`, or `.prewarm`. Subsequent accesses return the same cached
|
|
6
|
+
* underlying backend.
|
|
7
|
+
*
|
|
8
|
+
* Used by `defaultBackend()` for env-conditional selection, and by the
|
|
9
|
+
* authored-definition normalizer when an author passes a callback to
|
|
10
|
+
* `SandboxDefinition.backend` (e.g. `backend: () => vercelBackend({...})`)
|
|
11
|
+
* so the factory runs at first use rather than at module load — while
|
|
12
|
+
* still preserving backend-internal state (such as the Vercel backend's
|
|
13
|
+
* prewarmed-templates map) across every framework call.
|
|
14
|
+
*/
|
|
15
|
+
export declare function lazyBackend<BO, SO>(factory: () => SandboxBackend<BO, SO>): SandboxBackend<BO, SO>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a backend-producing function in a `SandboxBackend` proxy that
|
|
3
|
+
* invokes the function exactly once, on first access to any of `.name`,
|
|
4
|
+
* `.create`, or `.prewarm`. Subsequent accesses return the same cached
|
|
5
|
+
* underlying backend.
|
|
6
|
+
*
|
|
7
|
+
* Used by `defaultBackend()` for env-conditional selection, and by the
|
|
8
|
+
* authored-definition normalizer when an author passes a callback to
|
|
9
|
+
* `SandboxDefinition.backend` (e.g. `backend: () => vercelBackend({...})`)
|
|
10
|
+
* so the factory runs at first use rather than at module load — while
|
|
11
|
+
* still preserving backend-internal state (such as the Vercel backend's
|
|
12
|
+
* prewarmed-templates map) across every framework call.
|
|
13
|
+
*/
|
|
14
|
+
export function lazyBackend(factory) {
|
|
15
|
+
let resolved;
|
|
16
|
+
function resolve() {
|
|
17
|
+
if (resolved === undefined) {
|
|
18
|
+
resolved = factory();
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
get name() {
|
|
24
|
+
return resolve().name;
|
|
25
|
+
},
|
|
26
|
+
create(input) {
|
|
27
|
+
return resolve().create(input);
|
|
28
|
+
},
|
|
29
|
+
async prewarm(input) {
|
|
30
|
+
await resolve().prewarm(input);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -30,7 +30,7 @@ export async function executeReadFileOnSandbox(args) {
|
|
|
30
30
|
throw new Error(`offset must be >= 1. Received: ${effectiveOffset}.`);
|
|
31
31
|
}
|
|
32
32
|
// ── Read full file for fingerprinting ───────────────────────────────
|
|
33
|
-
const rawContent = await sandbox.readTextFile(filePath);
|
|
33
|
+
const rawContent = await sandbox.readTextFile({ path: filePath });
|
|
34
34
|
if (rawContent === null) {
|
|
35
35
|
throw new Error(`File not found: ${filePath}. Verify the path exists and is accessible in the sandbox.`);
|
|
36
36
|
}
|
|
@@ -31,6 +31,6 @@ export async function ripgrepIsAvailable(session) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
async function runProbe(session) {
|
|
34
|
-
const result = await session.runCommand("command -v rg >/dev/null 2>&1");
|
|
34
|
+
const result = await session.runCommand({ command: "command -v rg >/dev/null 2>&1" });
|
|
35
35
|
return result.exitCode === 0;
|
|
36
36
|
}
|
|
@@ -9,41 +9,62 @@ import { bufferToStream, streamToBuffer } from "./stream-utils.js";
|
|
|
9
9
|
export function buildSandboxSession(primitives) {
|
|
10
10
|
return {
|
|
11
11
|
id: primitives.id,
|
|
12
|
-
|
|
13
|
-
return
|
|
12
|
+
resolvePath(path) {
|
|
13
|
+
return primitives.resolvePath(path);
|
|
14
|
+
},
|
|
15
|
+
async runCommand(options) {
|
|
16
|
+
return await primitives.runCommand(options);
|
|
14
17
|
},
|
|
15
|
-
async
|
|
16
|
-
|
|
18
|
+
async readFile(options) {
|
|
19
|
+
return await primitives.readFile({
|
|
20
|
+
abortSignal: options.abortSignal,
|
|
21
|
+
path: primitives.resolvePath(options.path),
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
async readBinaryFile(options) {
|
|
25
|
+
const stream = await primitives.readFile({
|
|
26
|
+
abortSignal: options.abortSignal,
|
|
27
|
+
path: primitives.resolvePath(options.path),
|
|
28
|
+
});
|
|
17
29
|
if (stream === null) {
|
|
18
30
|
return null;
|
|
19
31
|
}
|
|
20
32
|
return await streamToBuffer(stream);
|
|
21
33
|
},
|
|
22
|
-
async readTextFile(
|
|
34
|
+
async readTextFile(options) {
|
|
23
35
|
validateReadTextFileOptions(options);
|
|
24
|
-
const stream = await primitives.readFile(
|
|
36
|
+
const stream = await primitives.readFile({
|
|
37
|
+
abortSignal: options.abortSignal,
|
|
38
|
+
path: primitives.resolvePath(options.path),
|
|
39
|
+
});
|
|
25
40
|
if (stream === null) {
|
|
26
41
|
return null;
|
|
27
42
|
}
|
|
28
43
|
const buf = await streamToBuffer(stream);
|
|
29
|
-
const text =
|
|
44
|
+
const text = decodeBytes(buf, options.encoding ?? "utf-8");
|
|
30
45
|
return applyLineRange(text, options);
|
|
31
46
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
async writeFile(options) {
|
|
48
|
+
await primitives.writeFile({
|
|
49
|
+
abortSignal: options.abortSignal,
|
|
50
|
+
content: options.content,
|
|
51
|
+
path: primitives.resolvePath(options.path),
|
|
52
|
+
});
|
|
37
53
|
},
|
|
38
|
-
async
|
|
39
|
-
await primitives.writeFile(
|
|
54
|
+
async writeBinaryFile(options) {
|
|
55
|
+
await primitives.writeFile({
|
|
56
|
+
abortSignal: options.abortSignal,
|
|
57
|
+
content: bufferToStream(options.content),
|
|
58
|
+
path: primitives.resolvePath(options.path),
|
|
59
|
+
});
|
|
40
60
|
},
|
|
41
|
-
async
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
async writeTextFile(options) {
|
|
62
|
+
const buf = encodeString(options.content, options.encoding ?? "utf-8");
|
|
63
|
+
await primitives.writeFile({
|
|
64
|
+
abortSignal: options.abortSignal,
|
|
65
|
+
content: bufferToStream(buf),
|
|
66
|
+
path: primitives.resolvePath(options.path),
|
|
67
|
+
});
|
|
47
68
|
},
|
|
48
69
|
};
|
|
49
70
|
}
|
|
@@ -55,9 +76,6 @@ export function buildSandboxSession(primitives) {
|
|
|
55
76
|
* or when `startLine` exceeds `endLine`.
|
|
56
77
|
*/
|
|
57
78
|
function validateReadTextFileOptions(options) {
|
|
58
|
-
if (options === undefined) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
79
|
const { startLine, endLine } = options;
|
|
62
80
|
if (startLine !== undefined && (!Number.isInteger(startLine) || startLine < 1)) {
|
|
63
81
|
throw new Error("startLine must be a positive integer (1-based).");
|
|
@@ -102,39 +120,39 @@ function splitLinesPreservingEndings(content) {
|
|
|
102
120
|
* endings.
|
|
103
121
|
*/
|
|
104
122
|
function applyLineRange(content, options) {
|
|
105
|
-
if (options
|
|
123
|
+
if (options.startLine === undefined && options.endLine === undefined) {
|
|
106
124
|
return content;
|
|
107
125
|
}
|
|
108
126
|
const lines = splitLinesPreservingEndings(content);
|
|
109
127
|
const totalLines = lines.length;
|
|
110
|
-
const startLine = options
|
|
111
|
-
const endLine = Math.min(options
|
|
128
|
+
const startLine = options.startLine ?? 1;
|
|
129
|
+
const endLine = Math.min(options.endLine ?? totalLines, totalLines);
|
|
112
130
|
if (startLine > totalLines) {
|
|
113
131
|
return "";
|
|
114
132
|
}
|
|
115
133
|
return lines.slice(startLine - 1, endLine).join("");
|
|
116
134
|
}
|
|
117
135
|
/**
|
|
118
|
-
* Decodes
|
|
136
|
+
* Decodes raw bytes to a string using the given encoding.
|
|
119
137
|
*
|
|
120
138
|
* `"utf-8"` uses `TextDecoder` in fatal mode so malformed sequences
|
|
121
139
|
* throw. Other encodings fall back to Node's `Buffer.toString`.
|
|
122
140
|
*/
|
|
123
|
-
function
|
|
141
|
+
function decodeBytes(buf, encoding) {
|
|
124
142
|
if (encoding === "utf-8" || encoding === "utf8") {
|
|
125
143
|
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
126
144
|
}
|
|
127
|
-
return buf.toString(encoding);
|
|
145
|
+
return Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength).toString(encoding);
|
|
128
146
|
}
|
|
129
147
|
/**
|
|
130
|
-
* Encodes a string to
|
|
148
|
+
* Encodes a string to raw bytes using the given encoding.
|
|
131
149
|
*
|
|
132
150
|
* `"utf-8"` uses `TextEncoder`; other encodings fall back to
|
|
133
151
|
* `Buffer.from(str, encoding)`.
|
|
134
152
|
*/
|
|
135
153
|
function encodeString(str, encoding) {
|
|
136
154
|
if (encoding === "utf-8" || encoding === "utf8") {
|
|
137
|
-
return
|
|
155
|
+
return new TextEncoder().encode(str);
|
|
138
156
|
}
|
|
139
157
|
return Buffer.from(str, encoding);
|
|
140
158
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function streamToBuffer(stream: ReadableStream<Uint8Array>): Promise<Buffer>;
|
|
6
6
|
/**
|
|
7
|
-
* Wraps a
|
|
7
|
+
* Wraps a byte buffer as a single-chunk `ReadableStream<Uint8Array>`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function bufferToStream(buf:
|
|
9
|
+
export declare function bufferToStream(buf: Uint8Array): ReadableStream<Uint8Array>;
|
|
@@ -10,7 +10,7 @@ export async function streamToBuffer(stream) {
|
|
|
10
10
|
return Buffer.concat(chunks);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Wraps a
|
|
13
|
+
* Wraps a byte buffer as a single-chunk `ReadableStream<Uint8Array>`.
|
|
14
14
|
*/
|
|
15
15
|
export function bufferToStream(buf) {
|
|
16
16
|
return new ReadableStream({
|
|
@@ -24,10 +24,10 @@ export async function executeWriteFileOnSandbox(args) {
|
|
|
24
24
|
// cost: the entire file is read and hashed before every write. A
|
|
25
25
|
// separate `exists()` primitive would avoid this for new files but
|
|
26
26
|
// would require a sandbox session API change.
|
|
27
|
-
const currentContent = await sandbox.readTextFile(filePath);
|
|
27
|
+
const currentContent = await sandbox.readTextFile({ path: filePath });
|
|
28
28
|
if (currentContent === null) {
|
|
29
29
|
// ── File does not exist — write immediately, no prior read needed ──
|
|
30
|
-
await sandbox.writeTextFile(
|
|
30
|
+
await sandbox.writeTextFile({ content, path: filePath });
|
|
31
31
|
const freshStamp = createReadFileStamp({
|
|
32
32
|
content,
|
|
33
33
|
filePath: normalizedPath,
|
|
@@ -52,7 +52,7 @@ export async function executeWriteFileOnSandbox(args) {
|
|
|
52
52
|
"Please read the file again before modifying it.");
|
|
53
53
|
}
|
|
54
54
|
// ── Write and refresh stamp ─────────────────────────────────────────
|
|
55
|
-
await sandbox.writeTextFile(
|
|
55
|
+
await sandbox.writeTextFile({ content, path: filePath });
|
|
56
56
|
const freshStamp = createReadFileStamp({
|
|
57
57
|
content,
|
|
58
58
|
filePath: normalizedPath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunInput } from "#channel/types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Serializable input for the workflow entrypoint.
|
|
4
4
|
*
|
|
@@ -9,9 +9,7 @@ import type { UserContent } from "ai";
|
|
|
9
9
|
* the serialized record.
|
|
10
10
|
*/
|
|
11
11
|
export interface WorkflowEntryInput {
|
|
12
|
-
readonly input:
|
|
13
|
-
readonly message: string | UserContent;
|
|
14
|
-
};
|
|
12
|
+
readonly input: RunInput["input"];
|
|
15
13
|
readonly serializedContext: Record<string, unknown>;
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
@@ -41,7 +41,7 @@ export async function workflowEntry(input) {
|
|
|
41
41
|
capabilities,
|
|
42
42
|
initialInput: {
|
|
43
43
|
kind: "deliver",
|
|
44
|
-
payloads: [{ message: input.input.message }],
|
|
44
|
+
payloads: [{ message: input.input.message, modelContext: input.input.modelContext }],
|
|
45
45
|
},
|
|
46
46
|
mode,
|
|
47
47
|
serializedContext: input.serializedContext,
|
|
@@ -171,7 +171,7 @@ async function hydrateMessageContent(content, sandbox) {
|
|
|
171
171
|
if (!shouldInlineSandboxRefAsBytes(ref)) {
|
|
172
172
|
return renderSandboxRefAsTextPart(ref);
|
|
173
173
|
}
|
|
174
|
-
const bytes = await sandbox.readBinaryFile(ref.path);
|
|
174
|
+
const bytes = await sandbox.readBinaryFile({ path: ref.path });
|
|
175
175
|
if (bytes === null) {
|
|
176
176
|
throw new Error(`Sandbox-ref FilePart references missing file: "${ref.path}". ` +
|
|
177
177
|
"The staging pipeline invariant (every ash-sandbox: ref has bytes on disk) was violated.");
|
|
@@ -230,21 +230,20 @@ async function stageFilePart(part, sandbox, adapterCtx) {
|
|
|
230
230
|
}
|
|
231
231
|
return stageResolvedBytes(part, { bytes }, sandbox);
|
|
232
232
|
}
|
|
233
|
-
function stageResolvedBytes(part, resolved, sandbox) {
|
|
233
|
+
async function stageResolvedBytes(part, resolved, sandbox) {
|
|
234
234
|
const bytes = resolved.bytes;
|
|
235
235
|
const sha = sha256Prefix(bytes);
|
|
236
236
|
const mediaType = resolved.mediaType ?? part.mediaType ?? DEFAULT_MEDIA_TYPE;
|
|
237
237
|
const name = safeFilename(resolved.filename ?? part.filename, sha);
|
|
238
238
|
const authored = `${ATTACHMENTS_ROOT}/${sha}/${name}`;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
239
|
+
await sandbox.writeBinaryFile({ content: bytes, path: authored });
|
|
240
|
+
const resolvedPath = sandbox.resolvePath(authored);
|
|
241
|
+
return {
|
|
242
|
+
...part,
|
|
243
|
+
data: encodeSandboxRef({ mediaType, path: resolvedPath, size: bytes.byteLength }),
|
|
244
|
+
filename: resolvedPath,
|
|
245
|
+
mediaType,
|
|
246
|
+
};
|
|
248
247
|
}
|
|
249
248
|
async function tryFetchFile(url, adapterCtx) {
|
|
250
249
|
const adapter = adapterCtx.ctx.get(ChannelKey);
|
|
@@ -14,6 +14,10 @@ export function coalesceTurnInputs(a, b) {
|
|
|
14
14
|
a: a.message,
|
|
15
15
|
b: b.message,
|
|
16
16
|
});
|
|
17
|
+
const modelContext = coalesceModelContext({
|
|
18
|
+
a: a.modelContext,
|
|
19
|
+
b: b.modelContext,
|
|
20
|
+
});
|
|
17
21
|
const result = {};
|
|
18
22
|
if (inputResponses !== undefined) {
|
|
19
23
|
result.inputResponses = inputResponses;
|
|
@@ -21,6 +25,9 @@ export function coalesceTurnInputs(a, b) {
|
|
|
21
25
|
if (message !== undefined) {
|
|
22
26
|
result.message = message;
|
|
23
27
|
}
|
|
28
|
+
if (modelContext !== undefined) {
|
|
29
|
+
result.modelContext = modelContext;
|
|
30
|
+
}
|
|
24
31
|
return result;
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
@@ -73,6 +80,14 @@ function coalesceInputResponses(input) {
|
|
|
73
80
|
}
|
|
74
81
|
return [...a, ...b];
|
|
75
82
|
}
|
|
83
|
+
function coalesceModelContext(input) {
|
|
84
|
+
const a = input.a ?? [];
|
|
85
|
+
const b = input.b ?? [];
|
|
86
|
+
if (a.length === 0 && b.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return [...a, ...b];
|
|
90
|
+
}
|
|
76
91
|
/**
|
|
77
92
|
* Merges two optional turn messages into one.
|
|
78
93
|
*
|
|
@@ -26,6 +26,11 @@ export function buildToolSet(input) {
|
|
|
26
26
|
execute: definition.execute,
|
|
27
27
|
inputSchema: definition.inputSchema,
|
|
28
28
|
needsApproval: buildNeedsApprovalFn(definition, input),
|
|
29
|
+
...(definition.toModelOutput !== undefined
|
|
30
|
+
? {
|
|
31
|
+
toModelOutput: ({ output }) => definition.toModelOutput(output),
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
29
34
|
});
|
|
30
35
|
}
|
|
31
36
|
return tools;
|
|
@@ -79,13 +79,12 @@ export interface StepInput {
|
|
|
79
79
|
readonly inputResponses?: readonly InputResponse[];
|
|
80
80
|
readonly message?: string | UserContent;
|
|
81
81
|
/**
|
|
82
|
-
* Ephemeral messages
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* channels.
|
|
82
|
+
* Ephemeral messages appended to the next model call and never
|
|
83
|
+
* persisted to durable session history. Populated by channels via
|
|
84
|
+
* `SendPayload.modelContext` and by lifecycle hooks via the
|
|
85
|
+
* `modelContext` field on a `LifecycleHookResult`. Channel-provided
|
|
86
|
+
* messages appear first, then session-hook messages, then turn-hook
|
|
87
|
+
* messages.
|
|
89
88
|
*/
|
|
90
89
|
readonly modelContext?: readonly ModelMessage[];
|
|
91
90
|
/**
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.20.0";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
1
2
|
import type { SandboxDefinition } from "#public/definitions/sandbox.js";
|
|
2
|
-
export type NormalizedSandboxDefinition = Readonly<SandboxDefinition
|
|
3
|
+
export type NormalizedSandboxDefinition = Readonly<Omit<SandboxDefinition, "backend">> & {
|
|
4
|
+
readonly backend?: SandboxBackend;
|
|
3
5
|
readonly description?: string;
|
|
4
6
|
};
|
|
5
7
|
/**
|
|
6
8
|
* Normalizes one authored sandbox definition into the canonical internal
|
|
7
|
-
* shape.
|
|
9
|
+
* shape. If the author supplied a `backend` callback (e.g.
|
|
10
|
+
* `backend: () => vercelBackend({...})`), it is wrapped via
|
|
11
|
+
* {@link lazyBackend} so downstream consumers always see a plain
|
|
12
|
+
* `SandboxBackend` value — the callback fires exactly once on first
|
|
13
|
+
* access and the resulting backend is memoized.
|
|
8
14
|
*/
|
|
9
15
|
export declare function normalizeSandboxDefinition(value: unknown, message: string): NormalizedSandboxDefinition;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { expectFunction, expectObjectRecord, expectOnlyKnownKeys, } from "#internal/authored-module.js";
|
|
2
|
+
import { lazyBackend } from "#execution/sandbox/lazy-backend.js";
|
|
2
3
|
/**
|
|
3
4
|
* Normalizes one authored sandbox definition into the canonical internal
|
|
4
|
-
* shape.
|
|
5
|
+
* shape. If the author supplied a `backend` callback (e.g.
|
|
6
|
+
* `backend: () => vercelBackend({...})`), it is wrapped via
|
|
7
|
+
* {@link lazyBackend} so downstream consumers always see a plain
|
|
8
|
+
* `SandboxBackend` value — the callback fires exactly once on first
|
|
9
|
+
* access and the resulting backend is memoized.
|
|
5
10
|
*/
|
|
6
11
|
export function normalizeSandboxDefinition(value, message) {
|
|
7
12
|
const record = expectObjectRecord(value, message);
|
|
@@ -25,7 +30,10 @@ export function normalizeSandboxDefinition(value, message) {
|
|
|
25
30
|
return definition;
|
|
26
31
|
}
|
|
27
32
|
function expectSandboxBackend(value, message) {
|
|
28
|
-
|
|
33
|
+
if (typeof value === "function") {
|
|
34
|
+
return lazyBackend(value);
|
|
35
|
+
}
|
|
36
|
+
const record = expectObjectRecord(value, `${message} The "backend" field must be a SandboxBackend value (use vercelBackend(), localBackend(), or your own factory) or a zero-arg function returning one.`);
|
|
29
37
|
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
30
38
|
throw new Error(`${message} The "backend" value must expose a non-empty string "name" identifier.`);
|
|
31
39
|
}
|
|
@@ -12,7 +12,15 @@ export function normalizeToolDefinition(value, message) {
|
|
|
12
12
|
return { kind: "disabled" };
|
|
13
13
|
}
|
|
14
14
|
const record = expectObjectRecord(value, message);
|
|
15
|
-
expectOnlyKnownKeys(record, [
|
|
15
|
+
expectOnlyKnownKeys(record, [
|
|
16
|
+
"description",
|
|
17
|
+
"execute",
|
|
18
|
+
"inputSchema",
|
|
19
|
+
"needsApproval",
|
|
20
|
+
"onCompact",
|
|
21
|
+
"retentionPolicy",
|
|
22
|
+
"toModelOutput",
|
|
23
|
+
], message);
|
|
16
24
|
const inputSchema = record.inputSchema === undefined ? null : normalizeJsonSchemaDefinition(record.inputSchema);
|
|
17
25
|
const definition = {
|
|
18
26
|
description: expectString(record.description, message),
|
|
@@ -38,6 +46,9 @@ export function normalizeToolDefinition(value, message) {
|
|
|
38
46
|
throw new Error(`${message} Expected \`retentionPolicy\` to be "auto", "keep", or a function.`);
|
|
39
47
|
}
|
|
40
48
|
}
|
|
49
|
+
if (record.toModelOutput !== undefined) {
|
|
50
|
+
expectFunction(record.toModelOutput, message);
|
|
51
|
+
}
|
|
41
52
|
return {
|
|
42
53
|
kind: "tool",
|
|
43
54
|
definition,
|
|
@@ -107,7 +107,7 @@ function buildWorkflowFileHandlerSource(input) {
|
|
|
107
107
|
}
|
|
108
108
|
lines.push(`import { POST as ${handler.binding} } from ${JSON.stringify(handler.importSpecifier)};`);
|
|
109
109
|
}
|
|
110
|
-
lines.push(`import { getWorld as __ashGetWorkflowWorld } from ${JSON.stringify(input.runtimeImportSpecifier)};`, "", "try {", " const __ashWorkflowWorld = __ashGetWorkflowWorld();", ' if (typeof __ashWorkflowWorld?.registerHandler === "function") {');
|
|
110
|
+
lines.push(`import { getWorld as __ashGetWorkflowWorld } from ${JSON.stringify(input.runtimeImportSpecifier)};`, "", "try {", " const __ashWorkflowWorld = await __ashGetWorkflowWorld();", ' if (typeof __ashWorkflowWorld?.registerHandler === "function") {');
|
|
111
111
|
for (const handler of handlerBindings) {
|
|
112
112
|
lines.push(` __ashWorkflowWorld.registerHandler(${JSON.stringify(handler.queuePrefix)}, ${handler.binding});`);
|
|
113
113
|
}
|
|
@@ -67,4 +67,5 @@ export declare function bundleFinalWorkflowOutput(input: {
|
|
|
67
67
|
export declare function convertStepsManifest(steps: WorkflowManifest["steps"]): Record<string, unknown>;
|
|
68
68
|
export declare function convertWorkflowsManifest(workflows: WorkflowManifest["workflows"]): Record<string, unknown>;
|
|
69
69
|
export declare function convertClassesManifest(classes: WorkflowManifest["classes"]): Record<string, unknown>;
|
|
70
|
+
export declare function atomicWriteFile(targetPath: string, contents: string | Buffer | Uint8Array): Promise<void>;
|
|
70
71
|
export {};
|
|
@@ -360,3 +360,17 @@ function createManifestRelativeFilepath(workingDir, absolutePath) {
|
|
|
360
360
|
function isJavaScriptLikePath(path) {
|
|
361
361
|
return /\.(?:[cm]?[jt]sx?)$/.test(path);
|
|
362
362
|
}
|
|
363
|
+
/*
|
|
364
|
+
* Some generated workflow artifacts (notably `workflows.mjs`) are read by
|
|
365
|
+
* Nitro's Rolldown bundler concurrently with rebuilds during `ash dev`. A
|
|
366
|
+
* plain `writeFile` truncates the target first and streams bytes, so a
|
|
367
|
+
* reader can observe an empty or partial module mid-write and report
|
|
368
|
+
* spurious "missing export" errors. Writing to a sibling temp file and
|
|
369
|
+
* renaming relies on POSIX `rename` atomicity so readers always see
|
|
370
|
+
* either the old or the new contents.
|
|
371
|
+
*/
|
|
372
|
+
export async function atomicWriteFile(targetPath, contents) {
|
|
373
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
374
|
+
await writeFile(tmpPath, contents);
|
|
375
|
+
await rename(tmpPath, targetPath);
|
|
376
|
+
}
|