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,7 +4,7 @@ import { dirname, join, relative, resolve } from "node:path";
4
4
  import { resolvePackageSourceDirectoryPath, resolveWorkflowModulePath, } from "#internal/application/package.js";
5
5
  import { prepareAshVersionedCacheDirectory, writeAshVersionedCacheMetadata, } from "#internal/application/cache-metadata.js";
6
6
  import { normalizeEsmImportSpecifier } from "#internal/application/import-specifier.js";
7
- import { bundleFinalWorkflowOutput, collectWorkflowInputFiles, convertClassesManifest, convertStepsManifest, convertWorkflowsManifest, createAshPackageImportsPlugin, createWorkflowImport, createWorkflowPseudoPackagePlugin, createWorkflowTransformPlugin, createWorkflowVirtualEntryPlugin, WORKFLOW_VIRTUAL_ENTRY_ID, } from "#internal/workflow-bundle/builder-support.js";
7
+ import { atomicWriteFile, bundleFinalWorkflowOutput, collectWorkflowInputFiles, convertClassesManifest, convertStepsManifest, convertWorkflowsManifest, createAshPackageImportsPlugin, createWorkflowImport, createWorkflowPseudoPackagePlugin, createWorkflowTransformPlugin, createWorkflowVirtualEntryPlugin, WORKFLOW_VIRTUAL_ENTRY_ID, } from "#internal/workflow-bundle/builder-support.js";
8
8
  import { buildWithNitroRolldown, getSingleRolldownChunk, } from "#internal/bundler/nitro-rolldown.js";
9
9
  import { writeNitroStepEntrypoint } from "#internal/workflow-bundle/nitro-step-entry.js";
10
10
  import { copyNitroFunctionDirectory, createWorkflowFunctionEnvironment, retargetNitroFunctionDirectoryToWorkflowRoute, WORKFLOW_BUILDER_DEFERRED_PACKAGES, WORKFLOW_STEP_EXTERNAL_PACKAGES, } from "#internal/workflow-bundle/vercel-workflow-output.js";
@@ -347,12 +347,12 @@ async function addStepRegistrationsImport(workflowBundlePath, stepRegistrationsP
347
347
  ].join("\n");
348
348
  const firstImportMatch = source.match(/^import\s.+?;\n/m);
349
349
  if (firstImportMatch === null || firstImportMatch.index === undefined) {
350
- await writeFile(workflowBundlePath, `${importSource}${source}`);
350
+ await atomicWriteFile(workflowBundlePath, `${importSource}${source}`);
351
351
  return;
352
352
  }
353
353
  const insertionIndex = firstImportMatch.index + firstImportMatch[0].length;
354
354
  const nextSource = `${source.slice(0, insertionIndex)}${importSource}${source.slice(insertionIndex)}`;
355
- await writeFile(workflowBundlePath, nextSource);
355
+ await atomicWriteFile(workflowBundlePath, nextSource);
356
356
  }
357
357
  async function rewriteWorkflowRuntimeImports(filePath) {
358
358
  const source = await readTextFileIfPresent(filePath);
@@ -371,7 +371,7 @@ async function rewriteWorkflowRuntimeImports(filePath) {
371
371
  nextSource = replaceStringLiteralSpecifier(nextSource, specifier, resolvedSpecifier);
372
372
  }
373
373
  if (nextSource !== source) {
374
- await writeFile(filePath, nextSource);
374
+ await atomicWriteFile(filePath, nextSource);
375
375
  }
376
376
  }
377
377
  async function rewriteWorkflowCodeLiteral(filePath) {
@@ -394,7 +394,7 @@ async function rewriteWorkflowCodeLiteral(filePath) {
394
394
  const workflowCode = decodeWorkflowCodeTemplateLiteral(expression, filePath);
395
395
  const nextSource = `${source.slice(0, valueStart)}${encodeWorkflowCodeLiteral(workflowCode)}${source.slice(expressionEnd)}`;
396
396
  if (nextSource !== source) {
397
- await writeFile(filePath, nextSource);
397
+ await atomicWriteFile(filePath, nextSource);
398
398
  }
399
399
  }
400
400
  function encodeWorkflowCodeLiteral(workflowCode) {
@@ -468,5 +468,5 @@ async function mirrorFileBypassingUnlink(sourcePath, targetPath) {
468
468
  if (existingContents !== null && existingContents.equals(sourceContents)) {
469
469
  return;
470
470
  }
471
- await writeFile(targetPath, sourceContents);
471
+ await atomicWriteFile(targetPath, sourceContents);
472
472
  }
@@ -1,4 +1,4 @@
1
- export declare function encodeSlackApiBody(body: unknown, encoding: "form" | "json"): {
1
+ export declare function encodeSlackApiBody(body: unknown): {
2
2
  readonly body: string;
3
3
  readonly contentType: string;
4
4
  };
@@ -1,10 +1,4 @@
1
- export function encodeSlackApiBody(body, encoding) {
2
- if (encoding === "json") {
3
- return {
4
- body: JSON.stringify(body),
5
- contentType: "application/json; charset=utf-8",
6
- };
7
- }
1
+ export function encodeSlackApiBody(body) {
8
2
  const params = new URLSearchParams();
9
3
  if (body && typeof body === "object") {
10
4
  for (const [key, value] of Object.entries(body)) {
@@ -42,15 +42,16 @@ export interface SlackApiResponse {
42
42
  /**
43
43
  * Low-level POST to a Slack Web API method.
44
44
  *
45
- * Signs the request with the bot token. Returns Slack's raw JSON
46
- * response callers are expected to inspect `response.ok` themselves
47
- * when they care about success/failure semantics.
45
+ * Signs with the bot token and form-encodes the body — Slack documents
46
+ * `application/x-www-form-urlencoded` as universal, JSON support is
47
+ * partial (e.g. `conversations.replies` rejects JSON), so form is the
48
+ * only safe default. Returns Slack's raw JSON response; callers inspect
49
+ * `response.ok` themselves.
48
50
  */
49
51
  export declare function callSlackApi(input: {
50
52
  readonly botToken: SlackBotToken | undefined;
51
53
  readonly operation: string;
52
54
  readonly body: unknown;
53
- readonly encoding?: "form" | "json";
54
55
  }): Promise<SlackApiResponse>;
55
56
  /**
56
57
  * Builds the `request(op, body)` Slack API caller installed on every
@@ -43,13 +43,15 @@ export async function resolveSlackBotToken(token) {
43
43
  /**
44
44
  * Low-level POST to a Slack Web API method.
45
45
  *
46
- * Signs the request with the bot token. Returns Slack's raw JSON
47
- * response callers are expected to inspect `response.ok` themselves
48
- * when they care about success/failure semantics.
46
+ * Signs with the bot token and form-encodes the body — Slack documents
47
+ * `application/x-www-form-urlencoded` as universal, JSON support is
48
+ * partial (e.g. `conversations.replies` rejects JSON), so form is the
49
+ * only safe default. Returns Slack's raw JSON response; callers inspect
50
+ * `response.ok` themselves.
49
51
  */
50
52
  export async function callSlackApi(input) {
51
53
  const token = await resolveSlackBotToken(input.botToken);
52
- const encoded = encodeSlackApiBody(input.body, input.encoding ?? "json");
54
+ const encoded = encodeSlackApiBody(input.body);
53
55
  const response = await fetch(`https://slack.com/api/${input.operation}`, {
54
56
  method: "POST",
55
57
  headers: {
@@ -99,7 +101,6 @@ export function buildSlackBinding(input) {
99
101
  const getUrl = await callSlackApi({
100
102
  botToken: input.botToken,
101
103
  operation: "files.getUploadURLExternal",
102
- encoding: "form",
103
104
  body: {
104
105
  filename: file.filename,
105
106
  length: bytes.byteLength,
@@ -135,7 +136,6 @@ export function buildSlackBinding(input) {
135
136
  const complete = await callSlackApi({
136
137
  botToken: input.botToken,
137
138
  operation: "files.completeUploadExternal",
138
- encoding: "form",
139
139
  body: completeBody,
140
140
  });
141
141
  if (complete.ok !== true) {
@@ -1,6 +1,9 @@
1
+ export type { ModelMessage } from "ai";
1
2
  export { slackChannel, type SlackApiResponse, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelCredentials, type SlackChannelEvents, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackHandle, type SlackInboundResult, type SlackInboundResultOrPromise, type SlackInitialMessage, type SlackInteractionAction, type SlackMentionResult, type SlackMentionResultOrPromise, type SlackReceiveArgs, type SlackThread, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
2
3
  export type { SlackAttachment, SlackAuthor, SlackInboundContext, SlackMessage, } from "#public/channels/slack/inbound.js";
3
4
  export { slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
5
+ export { defaultSlackAuth } from "#public/channels/slack/defaults.js";
6
+ export { loadThreadContextMessages, type LoadThreadContextMessagesOptions, type ThreadContextSince, } from "#public/channels/slack/thread.js";
4
7
  export { cardToBlocks, cardToFallbackText, type BlockKitBlock, } from "#public/channels/slack/blocks.js";
5
8
  /**
6
9
  * Card builders and element types re-exported from the vendored chat
@@ -1,5 +1,7 @@
1
1
  export { slackChannel, } from "#public/channels/slack/slackChannel.js";
2
2
  export { slackContinuationToken, } from "#public/channels/slack/api.js";
3
+ export { defaultSlackAuth } from "#public/channels/slack/defaults.js";
4
+ export { loadThreadContextMessages, } from "#public/channels/slack/thread.js";
3
5
  export { cardToBlocks, cardToFallbackText, } from "#public/channels/slack/blocks.js";
4
6
  /**
5
7
  * Card builders and element types re-exported from the vendored chat
@@ -1,3 +1,4 @@
1
+ import type { ModelMessage } from "ai";
1
2
  import type { TypedReceiveRoute } from "#channel/receive-args.js";
2
3
  import type { SessionHandle } from "#channel/session.js";
3
4
  import type { SessionAuthContext } from "#channel/types.js";
@@ -158,12 +159,19 @@ export interface SlackInteractionUser {
158
159
  readonly name?: string;
159
160
  }
160
161
  /**
161
- * Result of an `onAppMention` or `onDirectMessage` callback. Return
162
- * `{ auth }` (auth may be `null`) to dispatch a turn with that session
163
- * auth context, or `null` to silently drop the inbound message.
162
+ * Result of an `onAppMention` or `onDirectMessage` callback. Return an
163
+ * object (auth may be `null`) to dispatch a turn, or `null` to silently
164
+ * drop the inbound message.
165
+ *
166
+ * `modelContext` mirrors lifecycle hook results: it contributes
167
+ * ephemeral messages to the next model call only, without writing them
168
+ * to durable session history. Use `role: "system"` for Slack thread
169
+ * background context so it lands before the user's mention; see
170
+ * `docs/public/channels/slack.md`.
164
171
  */
165
172
  export type SlackMentionResult = {
166
- auth: SessionAuthContext | null;
173
+ readonly auth: SessionAuthContext | null;
174
+ readonly modelContext?: readonly ModelMessage[];
167
175
  } | null;
168
176
  export type SlackMentionResultOrPromise = SlackMentionResult | Promise<SlackMentionResult>;
169
177
  /**
@@ -230,7 +230,10 @@ async function dispatchInboundMessage(input) {
230
230
  ? prependSlackContext(baseMessage, inboundContext)
231
231
  : baseMessage;
232
232
  try {
233
- await input.send(turnMessage, {
233
+ await input.send({
234
+ message: turnMessage,
235
+ modelContext: result.modelContext,
236
+ }, {
234
237
  auth: result.auth,
235
238
  continuationToken: slackContinuationToken(message.channelId, message.threadTs),
236
239
  state: {
@@ -0,0 +1,26 @@
1
+ import type { SlackThread, SlackThreadMessage } from "#public/channels/slack/api.js";
2
+ export type ThreadContextSince = "thread-root" | "last-agent-reply" | ((message: SlackThreadMessage) => boolean);
3
+ export interface LoadThreadContextMessagesOptions {
4
+ /**
5
+ * Boundary for returned context messages. Defaults to `"thread-root"`.
6
+ *
7
+ * Use `"last-agent-reply"` to include only user/thread messages
8
+ * since the last agent-authored Slack reply. Pass a predicate
9
+ * function for custom boundaries, such as "since the last message
10
+ * that mentioned a particular user".
11
+ */
12
+ readonly since?: ThreadContextSince;
13
+ }
14
+ /**
15
+ * Loads messages that are useful as background context for the current
16
+ * Slack thread turn.
17
+ *
18
+ * Returns an empty array when `message` is the thread root. For thread
19
+ * replies, refreshes the bound Slack thread and returns its recent
20
+ * messages before the triggering message, filtered by {@link options}.
21
+ * Formatting and model-message role choice stay with the caller.
22
+ */
23
+ export declare function loadThreadContextMessages(thread: Pick<SlackThread, "recentMessages" | "refresh">, message: {
24
+ readonly threadTs: string;
25
+ readonly ts: string;
26
+ }, options?: LoadThreadContextMessagesOptions): Promise<SlackThreadMessage[]>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Loads messages that are useful as background context for the current
3
+ * Slack thread turn.
4
+ *
5
+ * Returns an empty array when `message` is the thread root. For thread
6
+ * replies, refreshes the bound Slack thread and returns its recent
7
+ * messages before the triggering message, filtered by {@link options}.
8
+ * Formatting and model-message role choice stay with the caller.
9
+ */
10
+ export async function loadThreadContextMessages(thread, message, options = {}) {
11
+ if (isThreadRootMessage(message)) {
12
+ return [];
13
+ }
14
+ await thread.refresh();
15
+ const currentIndex = thread.recentMessages.findIndex((entry) => entry.ts === message.ts);
16
+ const candidateMessages = currentIndex === -1 ? thread.recentMessages : thread.recentMessages.slice(0, currentIndex);
17
+ const priorMessages = candidateMessages.filter((entry) => entry.threadTs === message.threadTs && entry.ts !== message.ts);
18
+ return applySinceBoundary(priorMessages, options.since);
19
+ }
20
+ function isThreadRootMessage(message) {
21
+ return message.threadTs === message.ts;
22
+ }
23
+ function findLastIndex(items, predicate) {
24
+ for (let index = items.length - 1; index >= 0; index -= 1) {
25
+ if (predicate(items[index])) {
26
+ return index;
27
+ }
28
+ }
29
+ return -1;
30
+ }
31
+ function applySinceBoundary(messages, since) {
32
+ const boundary = since ?? "thread-root";
33
+ if (typeof boundary === "function") {
34
+ const lastMatchingIndex = findLastIndex(messages, boundary);
35
+ return messages.slice(lastMatchingIndex + 1);
36
+ }
37
+ switch (boundary) {
38
+ case "thread-root":
39
+ return [...messages];
40
+ case "last-agent-reply": {
41
+ const lastAgentReplyIndex = findLastIndex(messages, (entry) => entry.isMe);
42
+ return messages.slice(lastAgentReplyIndex + 1);
43
+ }
44
+ }
45
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Optional } from "#shared/optional.js";
2
2
  import type { SandboxDefinition as SharedSandboxDefinition } from "#shared/sandbox-definition.js";
3
- export type { SandboxCommandOptions, SandboxCommandResult, SandboxReadTextFileOptions, SandboxSession, SandboxWriteTextFileOptions, } from "#shared/sandbox-session.js";
3
+ export type { SandboxCommandResult, SandboxReadBinaryFileOptions, SandboxReadFileOptions, SandboxReadTextFileOptions, SandboxRunCommandOptions, SandboxSession, SandboxWriteBinaryFileOptions, SandboxWriteFileOptions, SandboxWriteTextFileOptions, } from "#shared/sandbox-session.js";
4
4
  export type { SandboxBootstrapUseFn, SandboxSessionUseFn, SandboxBootstrapContext, SandboxSessionContext, } from "#shared/sandbox-definition.js";
5
5
  export type SandboxDefinition<BO = Record<string, never>, SO = Record<string, never>> = Optional<SharedSandboxDefinition<BO, SO>, "backend">;
6
6
  /**
@@ -47,6 +47,19 @@ export interface NeedsApprovalContext {
47
47
  * function. These are the same options the SDK passes to every tool call.
48
48
  */
49
49
  export type ToolExecuteOptions = Readonly<AiSdkToolExecuteOptions>;
50
+ /**
51
+ * Ash-owned shape for the model-facing tool result produced by
52
+ * {@link ToolDefinition.toModelOutput}. Structurally compatible with the
53
+ * AI SDK's `ToolResultOutput` so the harness can forward it without
54
+ * conversion.
55
+ */
56
+ export type ToolModelOutput = {
57
+ readonly type: "text";
58
+ readonly value: string;
59
+ } | {
60
+ readonly type: "json";
61
+ readonly value: unknown;
62
+ };
50
63
  /**
51
64
  * Result returned from a {@link ToolRetentionPolicy} function.
52
65
  *
@@ -106,6 +119,17 @@ export type ToolDefinition<TInput = unknown, TOutput = unknown> = PublicToolDefi
106
119
  * {@link ToolRetentionPolicy}.
107
120
  */
108
121
  retentionPolicy?: ToolRetentionPolicy<TOutput>;
122
+ /**
123
+ * Optional projection that controls what the model sees as the tool
124
+ * result. Receives the full `TOutput` from {@link execute} and returns
125
+ * a {@link ToolModelOutput} that becomes the model-facing tool result.
126
+ *
127
+ * When omitted, the model sees the full `execute` return value
128
+ * (default AI SDK serialization). Channel event handlers
129
+ * (`action.result`) always receive the full output regardless of
130
+ * whether this function is defined.
131
+ */
132
+ toModelOutput?: (output: TOutput) => ToolModelOutput | Promise<ToolModelOutput>;
109
133
  /**
110
134
  * Called after message-history compaction. The framework invokes this
111
135
  * hook for every tool that declares it, in tool registration order,
@@ -125,6 +149,7 @@ export declare function defineTool<TSchema extends StandardJSONSchemaV1<unknown,
125
149
  execute: ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, TOutput>["execute"];
126
150
  needsApproval?: ToolDefinition<unknown, unknown>["needsApproval"];
127
151
  retentionPolicy?: ToolDefinition<unknown, TOutput>["retentionPolicy"];
152
+ toModelOutput?: ToolDefinition<unknown, TOutput>["toModelOutput"];
128
153
  onCompact?: ToolDefinition<unknown, unknown>["onCompact"];
129
154
  }): ToolDefinition<StandardJSONSchemaV1.InferOutput<TSchema>, TOutput>;
130
155
  export declare function defineTool<TInput = unknown, TOutput = unknown>(definition: ToolDefinition<TInput, TOutput>): ToolDefinition<TInput, TOutput>;
@@ -1,7 +1,22 @@
1
1
  import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
2
+ import type { LocalSandboxCreateOptions } from "#public/sandbox/local-sandbox.js";
3
+ import type { VercelSandboxCreateOptions } from "#public/sandbox/vercel-sandbox.js";
4
+ /**
5
+ * Input to {@link defaultBackend}: a separate options bag per inner
6
+ * backend. The framework picks one bag at runtime based on `process.env.VERCEL`
7
+ * and passes it to the chosen factory; the other is ignored.
8
+ */
9
+ export interface DefaultBackendOptions {
10
+ readonly local?: LocalSandboxCreateOptions;
11
+ readonly vercel?: VercelSandboxCreateOptions;
12
+ }
2
13
  /**
3
14
  * Constructs an env-aware sandbox backend that delegates to
4
15
  * {@link vercelBackend} on hosted Vercel (where `process.env.VERCEL`
5
16
  * is truthy) and to {@link localBackend} everywhere else.
17
+ *
18
+ * Optionally accepts a keyed options bag (`{ local, vercel }`) so each
19
+ * inner backend receives its own typed create options without forcing
20
+ * authors to pin to one backend up front.
6
21
  */
7
- export declare function defaultBackend(): SandboxBackend;
22
+ export declare function defaultBackend(opts?: DefaultBackendOptions): SandboxBackend;
@@ -1,27 +1,15 @@
1
+ import { lazyBackend } from "#execution/sandbox/lazy-backend.js";
1
2
  import { localBackend } from "#public/sandbox/backends/local.js";
2
3
  import { vercelBackend } from "#public/sandbox/backends/vercel.js";
3
4
  /**
4
5
  * Constructs an env-aware sandbox backend that delegates to
5
6
  * {@link vercelBackend} on hosted Vercel (where `process.env.VERCEL`
6
7
  * is truthy) and to {@link localBackend} everywhere else.
8
+ *
9
+ * Optionally accepts a keyed options bag (`{ local, vercel }`) so each
10
+ * inner backend receives its own typed create options without forcing
11
+ * authors to pin to one backend up front.
7
12
  */
8
- export function defaultBackend() {
9
- let resolved;
10
- function resolve() {
11
- if (resolved === undefined) {
12
- resolved = process.env.VERCEL ? vercelBackend() : localBackend();
13
- }
14
- return resolved;
15
- }
16
- return {
17
- get name() {
18
- return resolve().name;
19
- },
20
- create(input) {
21
- return resolve().create(input);
22
- },
23
- async prewarm(input) {
24
- await resolve().prewarm(input);
25
- },
26
- };
13
+ export function defaultBackend(opts) {
14
+ return lazyBackend(() => process.env.VERCEL ? vercelBackend(opts?.vercel) : localBackend(opts?.local));
27
15
  }
@@ -1,4 +1,5 @@
1
1
  import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
2
+ import type { LocalSandboxCreateOptions } from "#public/sandbox/local-sandbox.js";
2
3
  /**
3
4
  * Constructs the built-in local sandbox backend.
4
5
  *
@@ -7,8 +8,10 @@ import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
7
8
  * under the application root. It is the default backend on developer
8
9
  * machines (`pnpm ash dev`).
9
10
  *
10
- * Takes no arguments today `just-bash` does not currently expose any
11
- * configuration that's worth surfacing through ash. New options would
12
- * be added here as the underlying runtime grows.
11
+ * Accepts an `opts` parameter for parity with other backends, but
12
+ * `LocalSandboxCreateOptions` is empty today `just-bash` does not
13
+ * currently expose any configuration worth surfacing. New options would
14
+ * be added by widening `LocalSandboxCreateOptions` and routing them
15
+ * into the binding without changing this signature.
13
16
  */
14
- export declare function localBackend(): SandboxBackend;
17
+ export declare function localBackend(opts?: LocalSandboxCreateOptions): SandboxBackend;
@@ -7,10 +7,12 @@ import { createLocalSandboxBackend } from "#execution/sandbox/bindings/local.js"
7
7
  * under the application root. It is the default backend on developer
8
8
  * machines (`pnpm ash dev`).
9
9
  *
10
- * Takes no arguments today `just-bash` does not currently expose any
11
- * configuration that's worth surfacing through ash. New options would
12
- * be added here as the underlying runtime grows.
10
+ * Accepts an `opts` parameter for parity with other backends, but
11
+ * `LocalSandboxCreateOptions` is empty today `just-bash` does not
12
+ * currently expose any configuration worth surfacing. New options would
13
+ * be added by widening `LocalSandboxCreateOptions` and routing them
14
+ * into the binding without changing this signature.
13
15
  */
14
- export function localBackend() {
15
- return createLocalSandboxBackend();
16
+ export function localBackend(opts) {
17
+ return createLocalSandboxBackend({ createOptions: opts });
16
18
  }
@@ -1,11 +1,17 @@
1
1
  import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
2
- import type { VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
2
+ import type { VercelSandboxBootstrapUseOptions, VercelSandboxCreateOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
3
3
  /**
4
4
  * Constructs the built-in Vercel sandbox backend.
5
5
  *
6
+ * The optional `opts` parameter is forwarded to the Vercel SDK's
7
+ * `Sandbox.create(...)` for every fresh sandbox the framework creates
8
+ * (template at prewarm, session at first-time create). On resume
9
+ * (`Sandbox.get`), no create happens, so opts are not re-applied.
10
+ *
6
11
  * `bootstrap({ use })` applies its options to the template via
7
12
  * `sandbox.update(...)`; those settings persist into the snapshot.
8
13
  * `onSession({ use })` applies its options to the live session via the
9
- * SDK's `update` under the hood.
14
+ * SDK's `update` under the hood — overriding any overlapping field
15
+ * from `opts`.
10
16
  */
11
- export declare function vercelBackend(): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
17
+ export declare function vercelBackend(opts?: VercelSandboxCreateOptions): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
@@ -2,11 +2,17 @@ import { createVercelSandboxBackend } from "#execution/sandbox/bindings/vercel.j
2
2
  /**
3
3
  * Constructs the built-in Vercel sandbox backend.
4
4
  *
5
+ * The optional `opts` parameter is forwarded to the Vercel SDK's
6
+ * `Sandbox.create(...)` for every fresh sandbox the framework creates
7
+ * (template at prewarm, session at first-time create). On resume
8
+ * (`Sandbox.get`), no create happens, so opts are not re-applied.
9
+ *
5
10
  * `bootstrap({ use })` applies its options to the template via
6
11
  * `sandbox.update(...)`; those settings persist into the snapshot.
7
12
  * `onSession({ use })` applies its options to the live session via the
8
- * SDK's `update` under the hood.
13
+ * SDK's `update` under the hood — overriding any overlapping field
14
+ * from `opts`.
9
15
  */
10
- export function vercelBackend() {
11
- return createVercelSandboxBackend();
16
+ export function vercelBackend(opts) {
17
+ return createVercelSandboxBackend({ createOptions: opts });
12
18
  }
@@ -3,10 +3,11 @@
3
3
  * `agent/sandbox/sandbox.ts` when paired with a `workspace/` folder).
4
4
  */
5
5
  export { getSandbox } from "#context/accessors.js";
6
- export { defineSandbox, type SandboxBootstrapContext, type SandboxBootstrapUseFn, type SandboxCommandOptions, type SandboxCommandResult, type SandboxDefinition, type SandboxReadTextFileOptions, type SandboxSession, type SandboxSessionContext, type SandboxSessionUseFn, type SandboxWriteTextFileOptions, } from "#public/definitions/sandbox.js";
6
+ export { defineSandbox, type SandboxBootstrapContext, type SandboxBootstrapUseFn, type SandboxCommandResult, type SandboxDefinition, type SandboxReadBinaryFileOptions, type SandboxReadFileOptions, type SandboxReadTextFileOptions, type SandboxRunCommandOptions, type SandboxSession, type SandboxSessionContext, type SandboxSessionUseFn, type SandboxWriteBinaryFileOptions, type SandboxWriteFileOptions, type SandboxWriteTextFileOptions, } from "#public/definitions/sandbox.js";
7
7
  export type { SandboxBackend, SandboxBackendCreateInput, SandboxBackendHandle, SandboxBackendPrewarmInput, SandboxBackendRuntimeContext, SandboxBackendSessionState, SandboxSeedFile, } from "#public/definitions/sandbox-backend.js";
8
8
  export { SandboxTemplateNotProvisionedError } from "#public/definitions/sandbox-backend.js";
9
9
  export { defaultBackend } from "#public/sandbox/backends/default.js";
10
10
  export { localBackend } from "#public/sandbox/backends/local.js";
11
11
  export { vercelBackend } from "#public/sandbox/backends/vercel.js";
12
- export type { VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions, } from "#public/sandbox/vercel-sandbox.js";
12
+ export type { LocalSandboxCreateOptions } from "#public/sandbox/local-sandbox.js";
13
+ export type { VercelSandboxBootstrapUseOptions, VercelSandboxCreateOptions, VercelSandboxSessionUseOptions, } from "#public/sandbox/vercel-sandbox.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Options accepted by `localBackend(opts)`. Reserved for future
3
+ * widening: today the local backend exposes no consumer-controllable
4
+ * create options, so this is an empty object. The parameter exists on
5
+ * the factory so adding real fields here later is purely additive.
6
+ */
7
+ export type LocalSandboxCreateOptions = Record<string, never>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,16 @@
1
- import type { SandboxUpdateParams } from "#compiled/@vercel/sandbox/index.js";
1
+ import type { Sandbox as SdkSandbox, SandboxUpdateParams } from "#compiled/@vercel/sandbox/index.js";
2
+ /**
3
+ * Options accepted by `vercelBackend(opts)`. Forwarded directly to the
4
+ * Vercel SDK's `Sandbox.create(...)` for every fresh sandbox the
5
+ * framework creates (template at prewarm time, session at first-time
6
+ * session-create). Skipped on resume (`Sandbox.get`) since no create
7
+ * happens there.
8
+ *
9
+ * Framework-injected fields (`name`, `persistent`, `source`, `signal`)
10
+ * are excluded — the framework owns those and overrides any author-
11
+ * supplied values.
12
+ */
13
+ export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkSandbox.create>[0]>, "name" | "persistent" | "source" | "signal">;
2
14
  /**
3
15
  * Options accepted by the Vercel backend's `bootstrap({ use })` hook.
4
16
  * Aliases the Vercel SDK's `SandboxUpdateParams` because bootstrap
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool authoring helpers for `agent/tools/*.ts` files.
3
3
  */
4
- export { type CompactionHookInput, type CompactionHookResult, type DisabledToolSentinel, defineTool, disableTool, isDisabledToolSentinel, type NeedsApprovalContext, type RetentionSummary, type ToolDefinition, type ToolRetentionPolicy, } from "#public/definitions/tool.js";
4
+ export { type CompactionHookInput, type CompactionHookResult, type DisabledToolSentinel, defineTool, disableTool, isDisabledToolSentinel, type NeedsApprovalContext, type RetentionSummary, type ToolDefinition, type ToolModelOutput, type ToolRetentionPolicy, } from "#public/definitions/tool.js";
5
5
  export { type DefineBashToolInput, defineBashTool } from "#public/tools/define-bash-tool.js";
6
6
  export { type DefineGlobToolInput, defineGlobTool } from "#public/tools/define-glob-tool.js";
7
7
  export { type DefineGrepToolInput, defineGrepTool } from "#public/tools/define-grep-tool.js";
@@ -1,3 +1,4 @@
1
+ import { lazyBackend } from "#execution/sandbox/lazy-backend.js";
1
2
  import { expectObjectRecord } from "#internal/authored-module.js";
2
3
  import { defaultBackend } from "#public/sandbox/backends/default.js";
3
4
  import { toErrorMessage } from "#shared/errors.js";
@@ -47,8 +48,11 @@ function resolveBackend(value, logicalPath) {
47
48
  if (value === undefined) {
48
49
  return defaultBackend();
49
50
  }
51
+ if (typeof value === "function") {
52
+ return lazyBackend(value);
53
+ }
50
54
  if (typeof value !== "object" || value === null) {
51
- throw new ResolveAgentError(`Sandbox "${logicalPath}" exposed a non-object "backend" field. Use vercelBackend(), localBackend(), or another factory that returns a SandboxBackend value.`, { logicalPath });
55
+ throw new ResolveAgentError(`Sandbox "${logicalPath}" exposed a non-object "backend" field. Use vercelBackend(), localBackend(), another factory that returns a SandboxBackend value, or a zero-arg callback returning one.`, { logicalPath });
52
56
  }
53
57
  const record = value;
54
58
  if (typeof record.name !== "string" || record.name.length === 0) {
@@ -60,6 +60,9 @@ function extractOptionalHooks(record, definition) {
60
60
  if (retentionPolicy !== undefined) {
61
61
  optional.retentionPolicy = retentionPolicy;
62
62
  }
63
+ if (record.toModelOutput !== undefined) {
64
+ optional.toModelOutput = expectFunction(record.toModelOutput, describe(definition, "to provide a toModelOutput function"));
65
+ }
63
66
  if (record.inputSchema !== undefined && isFlexibleSchema(record.inputSchema)) {
64
67
  optional.inputStandardSchema = record.inputSchema;
65
68
  }
@@ -27,7 +27,7 @@ export async function loadSkillFromSandbox(access, id) {
27
27
  assertSafeSkillId(id);
28
28
  const sandbox = await requireSandboxSession(access);
29
29
  const path = skillFilePath(id, "SKILL.md");
30
- const instructions = await sandbox.readTextFile(path);
30
+ const instructions = await sandbox.readTextFile({ path });
31
31
  if (instructions === null) {
32
32
  throw new Error(`No skill named "${id}" at ${path}.`);
33
33
  }
@@ -47,14 +47,14 @@ export function createSandboxSkillHandle(access, id) {
47
47
  return {
48
48
  async bytes() {
49
49
  const sandbox = await requireSandboxSession(access);
50
- const content = await sandbox.readBinaryFile(path);
50
+ const content = await sandbox.readBinaryFile({ path });
51
51
  if (content === null)
52
52
  throw new Error(`Skill file not found: ${path}`);
53
53
  return content;
54
54
  },
55
55
  async text() {
56
56
  const sandbox = await requireSandboxSession(access);
57
- const content = await sandbox.readTextFile(path);
57
+ const content = await sandbox.readTextFile({ path });
58
58
  if (content === null)
59
59
  throw new Error(`Skill file not found: ${path}`);
60
60
  return content;
@@ -5,7 +5,7 @@ import type { DiscoverDiagnosticsSummary } from "#discover/diagnostics.js";
5
5
  import type { ChannelMethod, RouteContext } from "#public/definitions/channel.js";
6
6
  import type { RouteHandler } from "#channel/routes.js";
7
7
  import type { LifecycleHooks, StreamEventHook } from "#public/definitions/hook.js";
8
- import type { CompactionHookInput, CompactionHookResult, NeedsApprovalContext, ToolRetentionPolicy } from "#public/definitions/tool.js";
8
+ import type { CompactionHookInput, CompactionHookResult, NeedsApprovalContext, ToolModelOutput, ToolRetentionPolicy } from "#public/definitions/tool.js";
9
9
  import type { AuthorizationDefinition, HeadersDefinition, ToolFilterDefinition } from "#runtime/connections/types.js";
10
10
  import type { CompiledWorkspaceResourceRoot } from "#compiler/manifest.js";
11
11
  import type { WorkspaceRuntimeSpec } from "#runtime/workspace/types.js";
@@ -15,6 +15,7 @@ import type { SourceRef, ModuleSourceRef, SkillPackageSourceRef, MarkdownSourceR
15
15
  import type { InternalSkillDefinition } from "#shared/skill-definition.js";
16
16
  import type { InternalAgentDefinition } from "#shared/agent-definition.js";
17
17
  import type { InternalToolDefinitionWithExecuteFn } from "#shared/tool-definition.js";
18
+ import type { SandboxBackend } from "#shared/sandbox-backend.js";
18
19
  import type { SandboxDefinition } from "#shared/sandbox-definition.js";
19
20
  /**
20
21
  * Runtime-owned source ref describing one additive config module import.
@@ -82,7 +83,15 @@ export interface ResolvedConnectionDefinition extends ResolvedModuleSourceRef {
82
83
  * `vercelBackend()` and `localBackend()` based on the current
83
84
  * environment).
84
85
  */
85
- export type ResolvedSandboxDefinition = Readonly<SandboxDefinition> & ResolvedModuleSourceRef & {
86
+ export type ResolvedSandboxDefinition = Readonly<Omit<SandboxDefinition, "backend">> & ResolvedModuleSourceRef & {
87
+ /**
88
+ * Resolved backend value. The authored `SandboxDefinition.backend`
89
+ * accepts either a `SandboxBackend` or a `() => SandboxBackend`; by
90
+ * the time it reaches the runtime the function form has been
91
+ * unwrapped via `lazyBackend(...)` so consumers always see a plain
92
+ * value.
93
+ */
94
+ readonly backend: SandboxBackend;
86
95
  readonly description?: string;
87
96
  };
88
97
  /**
@@ -127,6 +136,12 @@ export type ResolvedToolDefinition = Readonly<Optional<InternalToolDefinitionWit
127
136
  * history. See `packages/ash/src/execution/tool-compaction.ts`.
128
137
  */
129
138
  readonly onCompact?: (input: CompactionHookInput) => CompactionHookResult | Promise<CompactionHookResult> | void | Promise<void>;
139
+ /**
140
+ * Optional projection that controls what the model sees as the tool
141
+ * result. The full `execute` return is still visible to channel event
142
+ * handlers and the stream. See {@link ToolModelOutput}.
143
+ */
144
+ readonly toModelOutput?: (output: unknown) => ToolModelOutput | Promise<ToolModelOutput>;
130
145
  };
131
146
  /**
132
147
  * Runtime-owned authored hook definition resolved from a compiled module