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.
Files changed (113) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/docs/internals/context.md +7 -0
  3. package/dist/docs/public/channels/README.md +5 -0
  4. package/dist/docs/public/channels/slack.md +58 -4
  5. package/dist/docs/public/hooks.md +4 -2
  6. package/dist/docs/public/sandbox.md +71 -49
  7. package/dist/docs/public/tools.md +55 -3
  8. package/dist/docs/public/typescript-api.md +6 -1
  9. package/dist/src/channel/adapter.js +12 -2
  10. package/dist/src/channel/routes.d.ts +9 -1
  11. package/dist/src/channel/send.js +3 -3
  12. package/dist/src/channel/types.d.ts +3 -1
  13. package/dist/src/chunks/{dev-authored-source-watcher-j7YWh2Gx.js → dev-authored-source-watcher-NA8BZIXr.js} +1 -1
  14. package/dist/src/chunks/host-DOH_6p0f.js +22 -0
  15. package/dist/src/chunks/{paths-Dwv0Eash.js → paths-CbL9m08U.js} +25 -25
  16. package/dist/src/chunks/{prewarm-CQYfka30.js → prewarm-Bz6Jr9J1.js} +1 -1
  17. package/dist/src/cli/commands/info.js +1 -1
  18. package/dist/src/cli/run.js +1 -1
  19. package/dist/src/compiled/.vendor-stamp.json +7 -7
  20. package/dist/src/compiled/@ai-sdk/anthropic/index.js +2 -2
  21. package/dist/src/compiled/@ai-sdk/anthropic/package.json +1 -1
  22. package/dist/src/compiled/@ai-sdk/google/index.js +6 -6
  23. package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
  24. package/dist/src/compiled/@ai-sdk/mcp/index.js +1 -1
  25. package/dist/src/compiled/@ai-sdk/mcp/package.json +1 -1
  26. package/dist/src/compiled/@ai-sdk/openai/index.js +6 -10
  27. package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
  28. package/dist/src/compiled/@ai-sdk/otel/index.js +2 -2
  29. package/dist/src/compiled/@ai-sdk/otel/package.json +1 -1
  30. package/dist/src/compiled/@ai-sdk/provider/package.json +1 -1
  31. package/dist/src/compiled/@vercel/sandbox/index.d.ts +6 -1
  32. package/dist/src/compiled/@workflow/core/index.js +1 -1
  33. package/dist/src/compiled/@workflow/core/runtime.js +5 -5
  34. package/dist/src/compiled/@workflow/core/workflow.js +1 -1
  35. package/dist/src/compiled/@workflow/errors/index.js +1 -1
  36. package/dist/src/compiled/_chunks/workflow/{context-errors-CmtmBosi.js → context-errors-zbKocOyk.js} +1 -1
  37. package/dist/src/compiled/_chunks/workflow/dist-CpUQh3NH.js +14 -0
  38. package/dist/src/compiled/_chunks/workflow/{resume-hook-BqY8TqOE.js → resume-hook-CL8Ed91K.js} +2 -2
  39. package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +1 -0
  40. package/dist/src/context/hook-lifecycle.js +5 -1
  41. package/dist/src/evals/cli/eval.js +1 -1
  42. package/dist/src/execution/node-step.js +1 -0
  43. package/dist/src/execution/sandbox/bash-tool.js +1 -1
  44. package/dist/src/execution/sandbox/bindings/local.d.ts +14 -1
  45. package/dist/src/execution/sandbox/bindings/local.js +22 -16
  46. package/dist/src/execution/sandbox/bindings/vercel.d.ts +6 -0
  47. package/dist/src/execution/sandbox/bindings/vercel.js +25 -14
  48. package/dist/src/execution/sandbox/glob-tool.js +1 -1
  49. package/dist/src/execution/sandbox/grep-tool.js +1 -1
  50. package/dist/src/execution/sandbox/lazy-backend.d.ts +15 -0
  51. package/dist/src/execution/sandbox/lazy-backend.js +33 -0
  52. package/dist/src/execution/sandbox/read-file-tool.js +1 -1
  53. package/dist/src/execution/sandbox/ripgrep-probe.js +1 -1
  54. package/dist/src/execution/sandbox/session.js +49 -31
  55. package/dist/src/execution/sandbox/stream-utils.d.ts +2 -2
  56. package/dist/src/execution/sandbox/stream-utils.js +1 -1
  57. package/dist/src/execution/sandbox/write-file-tool.js +3 -3
  58. package/dist/src/execution/skills/types.d.ts +1 -1
  59. package/dist/src/execution/workflow-entry.d.ts +2 -4
  60. package/dist/src/execution/workflow-entry.js +1 -1
  61. package/dist/src/harness/attachment-staging.js +10 -11
  62. package/dist/src/harness/execute-tool.d.ts +1 -0
  63. package/dist/src/harness/messages.js +15 -0
  64. package/dist/src/harness/tools.js +5 -0
  65. package/dist/src/harness/types.d.ts +6 -7
  66. package/dist/src/internal/application/package.js +1 -1
  67. package/dist/src/internal/authored-definition/sandbox.d.ts +8 -2
  68. package/dist/src/internal/authored-definition/sandbox.js +10 -2
  69. package/dist/src/internal/authored-definition/schema-backed.js +12 -1
  70. package/dist/src/internal/nitro/host/configure-nitro-routes.js +1 -1
  71. package/dist/src/internal/workflow-bundle/builder-support.d.ts +1 -0
  72. package/dist/src/internal/workflow-bundle/builder-support.js +14 -0
  73. package/dist/src/internal/workflow-bundle/builder.js +6 -6
  74. package/dist/src/public/channels/slack/api-encoding.d.ts +1 -1
  75. package/dist/src/public/channels/slack/api-encoding.js +1 -7
  76. package/dist/src/public/channels/slack/api.d.ts +5 -4
  77. package/dist/src/public/channels/slack/api.js +6 -6
  78. package/dist/src/public/channels/slack/index.d.ts +3 -0
  79. package/dist/src/public/channels/slack/index.js +2 -0
  80. package/dist/src/public/channels/slack/slackChannel.d.ts +12 -4
  81. package/dist/src/public/channels/slack/slackChannel.js +4 -1
  82. package/dist/src/public/channels/slack/thread.d.ts +26 -0
  83. package/dist/src/public/channels/slack/thread.js +45 -0
  84. package/dist/src/public/definitions/sandbox.d.ts +1 -1
  85. package/dist/src/public/definitions/tool.d.ts +25 -0
  86. package/dist/src/public/sandbox/backends/default.d.ts +16 -1
  87. package/dist/src/public/sandbox/backends/default.js +7 -19
  88. package/dist/src/public/sandbox/backends/local.d.ts +7 -4
  89. package/dist/src/public/sandbox/backends/local.js +7 -5
  90. package/dist/src/public/sandbox/backends/vercel.d.ts +9 -3
  91. package/dist/src/public/sandbox/backends/vercel.js +9 -3
  92. package/dist/src/public/sandbox/index.d.ts +3 -2
  93. package/dist/src/public/sandbox/local-sandbox.d.ts +7 -0
  94. package/dist/src/public/sandbox/local-sandbox.js +1 -0
  95. package/dist/src/public/sandbox/vercel-sandbox.d.ts +13 -1
  96. package/dist/src/public/tools/index.d.ts +1 -1
  97. package/dist/src/runtime/resolve-sandbox.js +5 -1
  98. package/dist/src/runtime/resolve-tool.js +3 -0
  99. package/dist/src/runtime/skills/sandbox-access.js +3 -3
  100. package/dist/src/runtime/types.d.ts +17 -2
  101. package/dist/src/shared/sandbox-definition.d.ts +16 -1
  102. package/dist/src/shared/sandbox-session.d.ts +53 -121
  103. package/package.json +9 -9
  104. package/dist/src/chunks/host-C19hLVqS.js +0 -22
  105. package/dist/src/compiled/_chunks/workflow/dist-4zn5tehu.js +0 -10
  106. package/dist/src/compiled/_chunks/workflow/dist-DTWUhyDN.js +0 -5
  107. package/dist/src/compiled/_chunks/workflow/sleep-D30F1GSr.js +0 -1
  108. /package/dist/src/compiled/_chunks/workflow/{dist-B6aByiku.js → dist-0iNBqPYp.js} +0 -0
  109. /package/dist/src/compiled/_chunks/workflow/{dist-CVo7knbW.js → dist-D774SUM4.js} +0 -0
  110. /package/dist/src/compiled/_chunks/workflow/{src-Bc9OYRaN.js → src-ClRYdO4-.js} +0 -0
  111. /package/dist/src/compiled/_chunks/workflow/{symbols-DkV1V0kM.js → symbols-D-4tVV8x.js} +0 -0
  112. /package/dist/src/compiled/_chunks/workflow/{token-Cq5QjRq8.js → token-CsNmv7KW.js} +0 -0
  113. /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.path, file.content);
139
+ await templateSession.writeTextFile({ content: file.content, path: file.path });
134
140
  }
135
141
  else {
136
- await templateSession.writeBinaryFile(file.path, file.content);
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
- ...input.createOptions,
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(command, options) {
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?.workingDirectory ?? WORKSPACE_ROOT,
196
- signal: options?.abortSignal,
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 writeFile(resolvedPath, content) {
202
- const bytes = await streamToBuffer(content);
203
- await sandbox.writeFiles([{ content: bytes, path: resolvedPath }]);
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
- async readFile(path) {
13
- return await primitives.readFile(primitives.resolvePath(path));
12
+ resolvePath(path) {
13
+ return primitives.resolvePath(path);
14
+ },
15
+ async runCommand(options) {
16
+ return await primitives.runCommand(options);
14
17
  },
15
- async readBinaryFile(path) {
16
- const stream = await primitives.readFile(primitives.resolvePath(path));
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(path, options) {
34
+ async readTextFile(options) {
23
35
  validateReadTextFileOptions(options);
24
- const stream = await primitives.readFile(primitives.resolvePath(path));
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 = decodeBuffer(buf, options?.encoding ?? "utf-8");
44
+ const text = decodeBytes(buf, options.encoding ?? "utf-8");
30
45
  return applyLineRange(text, options);
31
46
  },
32
- resolvePath(path) {
33
- return primitives.resolvePath(path);
34
- },
35
- async runCommand(command, options) {
36
- return await primitives.runCommand(command, options);
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 writeFile(path, content) {
39
- await primitives.writeFile(primitives.resolvePath(path), content);
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 writeBinaryFile(path, content) {
42
- await primitives.writeFile(primitives.resolvePath(path), bufferToStream(content));
43
- },
44
- async writeTextFile(path, content, options) {
45
- const buf = encodeString(content, options?.encoding ?? "utf-8");
46
- await primitives.writeFile(primitives.resolvePath(path), bufferToStream(buf));
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?.startLine === undefined && options?.endLine === undefined) {
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?.startLine ?? 1;
111
- const endLine = Math.min(options?.endLine ?? totalLines, totalLines);
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 a Buffer to a string using the given encoding.
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 decodeBuffer(buf, encoding) {
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 a Buffer using the given encoding.
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 Buffer.from(new TextEncoder().encode(str));
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 Buffer as a single-chunk `ReadableStream<Uint8Array>`.
7
+ * Wraps a byte buffer as a single-chunk `ReadableStream<Uint8Array>`.
8
8
  */
9
- export declare function bufferToStream(buf: Buffer): ReadableStream<Uint8Array>;
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 Buffer as a single-chunk `ReadableStream<Uint8Array>`.
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(filePath, content);
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(filePath, content);
55
+ await sandbox.writeTextFile({ content, path: filePath });
56
56
  const freshStamp = createReadFileStamp({
57
57
  content,
58
58
  filePath: normalizedPath,
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export interface SkillFile {
5
5
  /** Reads the file content as bytes. */
6
- bytes(): Promise<Buffer>;
6
+ bytes(): Promise<Uint8Array>;
7
7
  /** Reads the file content as UTF-8 text. */
8
8
  text(): Promise<string>;
9
9
  }
@@ -1,4 +1,4 @@
1
- import type { UserContent } from "ai";
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
- return sandbox.writeBinaryFile(authored, bytes).then(() => {
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
- };
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);
@@ -22,4 +22,5 @@ export interface HarnessToolDefinition {
22
22
  readonly name: string;
23
23
  readonly needsApproval?: (ctx: NeedsApprovalContext) => boolean;
24
24
  readonly runtimeAction?: HarnessRuntimeActionDefinition;
25
+ readonly toModelOutput?: (output: unknown) => unknown;
25
26
  }
@@ -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 contributed by lifecycle hooks (via the
83
- * `modelContext` field on a {@link LifecycleHookResult}), appended to
84
- * the next model call's message list and never persisted to durable
85
- * session history.
86
- *
87
- * Internal to the execution/harness boundary; never produced by
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.18.3";
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
- const record = expectObjectRecord(value, `${message} The "backend" field must be a SandboxBackend value (use vercelBackend(), localBackend(), or your own factory).`);
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, ["description", "execute", "inputSchema", "needsApproval", "onCompact", "retentionPolicy"], message);
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
+ }