experimental-ash 0.18.2 → 0.19.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 (65) hide show
  1. package/CHANGELOG.md +14 -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/typescript-api.md +6 -1
  8. package/dist/src/channel/adapter.js +12 -2
  9. package/dist/src/channel/routes.d.ts +9 -1
  10. package/dist/src/channel/send.js +3 -3
  11. package/dist/src/channel/types.d.ts +3 -1
  12. package/dist/src/chunks/{dev-authored-source-watcher-j7YWh2Gx.js → dev-authored-source-watcher-L3_pagDa.js} +1 -1
  13. package/dist/src/chunks/{host-DkTSR6YJ.js → host-e2GUqnTr.js} +3 -3
  14. package/dist/src/chunks/{paths-Dwv0Eash.js → paths-BBleOGpa.js} +25 -25
  15. package/dist/src/chunks/{prewarm-CQYfka30.js → prewarm-DEymC5M-.js} +1 -1
  16. package/dist/src/cli/commands/info.js +1 -1
  17. package/dist/src/cli/run.js +1 -1
  18. package/dist/src/compiled/.vendor-stamp.json +1 -1
  19. package/dist/src/compiled/@vercel/sandbox/index.d.ts +6 -1
  20. package/dist/src/context/hook-lifecycle.js +5 -1
  21. package/dist/src/evals/cli/eval.js +1 -1
  22. package/dist/src/execution/sandbox/bindings/local.d.ts +14 -1
  23. package/dist/src/execution/sandbox/bindings/local.js +5 -1
  24. package/dist/src/execution/sandbox/bindings/vercel.d.ts +6 -0
  25. package/dist/src/execution/sandbox/bindings/vercel.js +12 -1
  26. package/dist/src/execution/sandbox/lazy-backend.d.ts +15 -0
  27. package/dist/src/execution/sandbox/lazy-backend.js +33 -0
  28. package/dist/src/execution/workflow-entry.d.ts +2 -4
  29. package/dist/src/execution/workflow-entry.js +1 -1
  30. package/dist/src/harness/messages.js +15 -0
  31. package/dist/src/harness/types.d.ts +6 -7
  32. package/dist/src/internal/application/package.js +1 -1
  33. package/dist/src/internal/authored-definition/sandbox.d.ts +8 -2
  34. package/dist/src/internal/authored-definition/sandbox.js +10 -2
  35. package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +0 -2
  36. package/dist/src/public/channels/slack/api.d.ts +2 -27
  37. package/dist/src/public/channels/slack/api.js +6 -82
  38. package/dist/src/public/channels/slack/defaults.js +2 -2
  39. package/dist/src/public/channels/slack/hitl.js +6 -3
  40. package/dist/src/public/channels/slack/inbound.js +1 -1
  41. package/dist/src/public/channels/slack/index.d.ts +3 -0
  42. package/dist/src/public/channels/slack/index.js +2 -0
  43. package/dist/src/public/channels/slack/limits.d.ts +19 -0
  44. package/dist/src/public/channels/slack/limits.js +23 -0
  45. package/dist/src/public/channels/slack/mrkdwn.d.ts +38 -0
  46. package/dist/src/public/channels/slack/mrkdwn.js +89 -0
  47. package/dist/src/public/channels/slack/slackChannel.d.ts +12 -4
  48. package/dist/src/public/channels/slack/slackChannel.js +5 -8
  49. package/dist/src/public/channels/slack/thread.d.ts +26 -0
  50. package/dist/src/public/channels/slack/thread.js +45 -0
  51. package/dist/src/public/definitions/defineChannel.d.ts +1 -1
  52. package/dist/src/public/sandbox/backends/default.d.ts +16 -1
  53. package/dist/src/public/sandbox/backends/default.js +7 -19
  54. package/dist/src/public/sandbox/backends/local.d.ts +7 -4
  55. package/dist/src/public/sandbox/backends/local.js +7 -5
  56. package/dist/src/public/sandbox/backends/vercel.d.ts +9 -3
  57. package/dist/src/public/sandbox/backends/vercel.js +9 -3
  58. package/dist/src/public/sandbox/index.d.ts +2 -1
  59. package/dist/src/public/sandbox/local-sandbox.d.ts +7 -0
  60. package/dist/src/public/sandbox/local-sandbox.js +1 -0
  61. package/dist/src/public/sandbox/vercel-sandbox.d.ts +13 -1
  62. package/dist/src/runtime/resolve-sandbox.js +5 -1
  63. package/dist/src/runtime/types.d.ts +10 -1
  64. package/dist/src/shared/sandbox-definition.d.ts +16 -1
  65. package/package.json +1 -1
@@ -11,7 +11,7 @@
11
11
  * picks whichever is set so the renderer can pick a widget kind on
12
12
  * UX grounds without changing the read path.
13
13
  */
14
- import { truncateModalTitle, truncatePlainText } from "#public/channels/slack/limits.js";
14
+ import { truncateModalTitle, truncatePlainText, truncateSectionText, } from "#public/channels/slack/limits.js";
15
15
  /**
16
16
  * Wire-format prefix every framework HITL widget mints onto its
17
17
  * `action_id`. Exposed so end-user adapters that render their own
@@ -93,7 +93,10 @@ export function isHitlAction(actionId) {
93
93
  * Always emits at least the prompt section.
94
94
  */
95
95
  export function renderInputRequestBlocks(request) {
96
- const prompt = { text: { text: request.prompt, type: "mrkdwn" }, type: "section" };
96
+ const prompt = {
97
+ text: { text: truncateSectionText(request.prompt), type: "mrkdwn" },
98
+ type: "section",
99
+ };
97
100
  const actionId = `${HITL_ACTION_PREFIX}${request.requestId}`;
98
101
  const options = request.options;
99
102
  const acceptsFreeform = request.allowFreeform === true || !options || options.length === 0;
@@ -146,7 +149,7 @@ export function renderInputRequestBlocks(request) {
146
149
  export function buildFreeformModalView(input) {
147
150
  const title = input.prompt ? truncateModalTitle(input.prompt) : "Your answer";
148
151
  const promptBlocks = input.prompt
149
- ? [{ type: "section", text: { type: "mrkdwn", text: input.prompt } }]
152
+ ? [{ type: "section", text: { type: "mrkdwn", text: truncateSectionText(input.prompt) } }]
150
153
  : [];
151
154
  return {
152
155
  type: "modal",
@@ -12,7 +12,7 @@
12
12
  * naming the actor, channel, and thread so the agent's prompt always
13
13
  * knows who and where it is talking.
14
14
  */
15
- import { slackMrkdwnToGfm } from "#public/channels/slack/api.js";
15
+ import { slackMrkdwnToGfm } from "#public/channels/slack/mrkdwn.js";
16
16
  /**
17
17
  * Parses a Slack `app_mention` event into a {@link SlackMessage}.
18
18
  *
@@ -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
@@ -20,6 +20,15 @@ export declare const SLACK_TYPING_STATUS_MAX_LENGTH = 50;
20
20
  * options and button labels are capped at 75 characters by Slack.
21
21
  */
22
22
  export declare const SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH = 75;
23
+ /**
24
+ * Block Kit `section` blocks cap `text.text` at 3000 chars. Anything
25
+ * longer fails the whole post with `invalid_blocks`.
26
+ */
27
+ export declare const SLACK_SECTION_TEXT_MAX_LENGTH = 3000;
28
+ /**
29
+ * Top-level `text` field on `chat.postMessage` is capped at 40000 chars.
30
+ */
31
+ export declare const SLACK_MESSAGE_TEXT_MAX_LENGTH = 40000;
23
32
  /**
24
33
  * `views.open` modal title is capped at 24 characters.
25
34
  */
@@ -37,6 +46,16 @@ export declare function truncateTypingStatus(status: string): string;
37
46
  */
38
47
  export declare function truncatePlainText(value: string): string;
39
48
  export declare function truncatePlainText(value: string | undefined): string | undefined;
49
+ /**
50
+ * Caps a section block's `text.text` at the Slack limit with a
51
+ * trailing ellipsis.
52
+ */
53
+ export declare function truncateSectionText(value: string): string;
54
+ /**
55
+ * Caps a `chat.postMessage` `text` field at the Slack limit with a
56
+ * trailing ellipsis.
57
+ */
58
+ export declare function truncateMessageText(value: string): string;
40
59
  /**
41
60
  * Caps a modal title at the Slack limit with a trailing ellipsis.
42
61
  */
@@ -20,6 +20,15 @@ export const SLACK_TYPING_STATUS_MAX_LENGTH = 50;
20
20
  * options and button labels are capped at 75 characters by Slack.
21
21
  */
22
22
  export const SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH = 75;
23
+ /**
24
+ * Block Kit `section` blocks cap `text.text` at 3000 chars. Anything
25
+ * longer fails the whole post with `invalid_blocks`.
26
+ */
27
+ export const SLACK_SECTION_TEXT_MAX_LENGTH = 3000;
28
+ /**
29
+ * Top-level `text` field on `chat.postMessage` is capped at 40000 chars.
30
+ */
31
+ export const SLACK_MESSAGE_TEXT_MAX_LENGTH = 40000;
23
32
  /**
24
33
  * `views.open` modal title is capped at 24 characters.
25
34
  */
@@ -38,6 +47,20 @@ export function truncatePlainText(value) {
38
47
  return undefined;
39
48
  return truncateWithEllipsis(value, SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH);
40
49
  }
50
+ /**
51
+ * Caps a section block's `text.text` at the Slack limit with a
52
+ * trailing ellipsis.
53
+ */
54
+ export function truncateSectionText(value) {
55
+ return truncateWithEllipsis(value, SLACK_SECTION_TEXT_MAX_LENGTH);
56
+ }
57
+ /**
58
+ * Caps a `chat.postMessage` `text` field at the Slack limit with a
59
+ * trailing ellipsis.
60
+ */
61
+ export function truncateMessageText(value) {
62
+ return truncateWithEllipsis(value, SLACK_MESSAGE_TEXT_MAX_LENGTH);
63
+ }
41
64
  /**
42
65
  * Caps a modal title at the Slack limit with a trailing ellipsis.
43
66
  */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Slack mrkdwn ↔ GitHub-flavored markdown converters and the bare
3
+ * `@mention` rewriter used by the outbound post pipeline. These are
4
+ * pure string utilities — no Slack API I/O, no I/O at all — so they
5
+ * live separately from the binding constructor and request shape code
6
+ * in {@link ./api.ts}.
7
+ */
8
+ /**
9
+ * Rewrites bare `@USER_ID` tokens (the form Slack apps and humans tend
10
+ * to type) into the `<@USER_ID>` mention syntax Slack actually renders.
11
+ * Anything already wrapped in `<...>` is left untouched.
12
+ */
13
+ export declare function rewriteBareMentions(text: string): string;
14
+ /**
15
+ * Best-effort GFM → Slack mrkdwn converter used only in contexts that
16
+ * do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
17
+ * `initial_comment` field).
18
+ *
19
+ * The main `{ markdown }` post path sends `markdown_text` directly
20
+ * to `chat.postMessage` and does not go through this converter.
21
+ */
22
+ export declare function gfmToSlackMrkdwn(input: string): string;
23
+ /**
24
+ * Best-effort Slack mrkdwn → GFM converter applied to the text of
25
+ * every inbound Slack message before the harness sees it.
26
+ *
27
+ * - `<@U123>` → `@U123`
28
+ * - `<#C123|name>` → `#name` (or `#C123` when no name)
29
+ * - `<!channel>` etc. → `@channel`
30
+ * - `<https://x|label>` → `[label](https://x)`
31
+ * - `<https://x>` → `https://x`
32
+ * - `*bold*` (paired) → `**bold**`
33
+ * - `~strike~` (paired) → `~~strike~~`
34
+ *
35
+ * Inline `_italic_` and code spans pass through unchanged because both
36
+ * formats render them identically.
37
+ */
38
+ export declare function slackMrkdwnToGfm(input: string): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Slack mrkdwn ↔ GitHub-flavored markdown converters and the bare
3
+ * `@mention` rewriter used by the outbound post pipeline. These are
4
+ * pure string utilities — no Slack API I/O, no I/O at all — so they
5
+ * live separately from the binding constructor and request shape code
6
+ * in {@link ./api.ts}.
7
+ */
8
+ const BARE_MENTION_RE = /(?<![<\w])@(\w+)/gu;
9
+ /**
10
+ * Rewrites bare `@USER_ID` tokens (the form Slack apps and humans tend
11
+ * to type) into the `<@USER_ID>` mention syntax Slack actually renders.
12
+ * Anything already wrapped in `<...>` is left untouched.
13
+ */
14
+ export function rewriteBareMentions(text) {
15
+ return text.replace(BARE_MENTION_RE, "<@$1>");
16
+ }
17
+ /**
18
+ * Best-effort GFM → Slack mrkdwn converter used only in contexts that
19
+ * do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
20
+ * `initial_comment` field).
21
+ *
22
+ * The main `{ markdown }` post path sends `markdown_text` directly
23
+ * to `chat.postMessage` and does not go through this converter.
24
+ */
25
+ export function gfmToSlackMrkdwn(input) {
26
+ const segments = splitCodeFences(input);
27
+ return segments
28
+ .map((segment) => (segment.kind === "code" ? segment.text : convertInline(segment.text)))
29
+ .join("");
30
+ }
31
+ /**
32
+ * Best-effort Slack mrkdwn → GFM converter applied to the text of
33
+ * every inbound Slack message before the harness sees it.
34
+ *
35
+ * - `<@U123>` → `@U123`
36
+ * - `<#C123|name>` → `#name` (or `#C123` when no name)
37
+ * - `<!channel>` etc. → `@channel`
38
+ * - `<https://x|label>` → `[label](https://x)`
39
+ * - `<https://x>` → `https://x`
40
+ * - `*bold*` (paired) → `**bold**`
41
+ * - `~strike~` (paired) → `~~strike~~`
42
+ *
43
+ * Inline `_italic_` and code spans pass through unchanged because both
44
+ * formats render them identically.
45
+ */
46
+ export function slackMrkdwnToGfm(input) {
47
+ const segments = splitCodeFences(input);
48
+ return segments
49
+ .map((segment) => (segment.kind === "code" ? segment.text : decodeInline(segment.text)))
50
+ .join("");
51
+ }
52
+ function splitCodeFences(input) {
53
+ const segments = [];
54
+ const fenceRe = /```[\s\S]*?```|`[^`\n]+`/gu;
55
+ let lastIndex = 0;
56
+ for (const match of input.matchAll(fenceRe)) {
57
+ const start = match.index ?? 0;
58
+ if (start > lastIndex) {
59
+ segments.push({ kind: "text", text: input.slice(lastIndex, start) });
60
+ }
61
+ segments.push({ kind: "code", text: match[0] });
62
+ lastIndex = start + match[0].length;
63
+ }
64
+ if (lastIndex < input.length) {
65
+ segments.push({ kind: "text", text: input.slice(lastIndex) });
66
+ }
67
+ return segments;
68
+ }
69
+ function convertInline(input) {
70
+ let out = input;
71
+ out = out.replace(/\*\*([^*\n]+)\*\*/gu, "*$1*");
72
+ out = out.replace(/__([^_\n]+)__/gu, "*$1*");
73
+ out = out.replace(/~~([^~\n]+)~~/gu, "~$1~");
74
+ out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/gu, "<$2|$1>");
75
+ return out;
76
+ }
77
+ function decodeInline(input) {
78
+ let out = input;
79
+ out = out.replace(/<!(channel|here|everyone)>/gu, "@$1");
80
+ out = out.replace(/<@([A-Z0-9]+)\|([^>]+)>/gu, "@$2");
81
+ out = out.replace(/<@([A-Z0-9]+)>/gu, "@$1");
82
+ out = out.replace(/<#([A-Z0-9]+)\|([^>]+)>/gu, "#$2");
83
+ out = out.replace(/<#([A-Z0-9]+)>/gu, "#$1");
84
+ out = out.replace(/<(https?:\/\/[^|>\s]+)\|([^>]+)>/gu, "[$2]($1)");
85
+ out = out.replace(/<(https?:\/\/[^>\s]+)>/gu, "$1");
86
+ out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/gu, "$1**$2**");
87
+ out = out.replace(/(^|[^~])~([^~\n]+)~(?!~)/gu, "$1~~$2~~");
88
+ return out;
89
+ }
@@ -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
  /**
@@ -14,13 +14,7 @@ function rebuildSlackContext(state, session, credentials) {
14
14
  channelId: state.channelId ?? "",
15
15
  threadTs: state.threadTs ?? "",
16
16
  teamId: state.teamId ?? undefined,
17
- // First `ctx.thread.post(...)` in a session that started without a
18
- // thread (programmatic start, schedule firing, etc.) becomes the
19
- // thread root. We update durable adapter state so subsequent
20
- // posts thread under it, and re-key the parked session so a
21
- // follow-up `@mention` reply in the same thread resumes the same
22
- // workflow.
23
- onAnchor(ts) {
17
+ onThreadTsChanged(ts) {
24
18
  state.threadTs = ts;
25
19
  if (state.channelId) {
26
20
  session.setContinuationToken(slackContinuationToken(state.channelId, ts));
@@ -236,7 +230,10 @@ async function dispatchInboundMessage(input) {
236
230
  ? prependSlackContext(baseMessage, inboundContext)
237
231
  : baseMessage;
238
232
  try {
239
- await input.send(turnMessage, {
233
+ await input.send({
234
+ message: turnMessage,
235
+ modelContext: result.modelContext,
236
+ }, {
240
237
  auth: result.auth,
241
238
  continuationToken: slackContinuationToken(message.channelId, message.threadTs),
242
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
+ }
@@ -37,7 +37,7 @@ export interface ChannelConfig<TState = undefined, TCtx = void> {
37
37
  * Builds the per-step channel context handed to `events` and
38
38
  * `deliver`. Receives the live {@link SessionHandle} so factories
39
39
  * that need to wire late-bound callbacks (e.g. Slack's auto-anchor
40
- * `onAnchor` calling `session.setContinuationToken(...)`) can close
40
+ * `onThreadTsChanged` calling `session.setContinuationToken(...)`) can close
41
41
  * over it. State mutations made inside the returned context flow
42
42
  * back through `adapter.state` automatically.
43
43
  */
@@ -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
  }
@@ -9,4 +9,5 @@ export { SandboxTemplateNotProvisionedError } from "#public/definitions/sandbox-
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,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) {
@@ -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
  /**