From b6df51d2bde0ad3b91f9faccb743826459fd6d44 Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Sat, 26 Sep 2026 04:44:30 +0000 Subject: [PATCH 1/4] Render integration capability descriptions in the execute inventory The execute tool description and skill listed connected integrations as bare slugs, so an agent seeing a fresh session could not tell what each integration is for without spending sandbox calls on discovery. Each inventory line now carries the integration's catalog description, truncated to one scannable line (120 chars, word edge, first line only). Legacy rows whose description merely repeats the slug or display name stay bare. Descriptions remain user-editable data (PATCH /api/integrations/:slug), so deployments tune the wording without a rebuild. parseIntegrationInventory tolerates the optional suffix, so hosts deriving search_ tools from the description see the same slugs. --- .../core/execution/src/description.test.ts | 60 +++++++++++++--- packages/core/execution/src/description.ts | 71 ++++++++++++++++--- 2 files changed, 109 insertions(+), 22 deletions(-) diff --git a/packages/core/execution/src/description.test.ts b/packages/core/execution/src/description.test.ts index 365f115585..80066e4a7e 100644 --- a/packages/core/execution/src/description.test.ts +++ b/packages/core/execution/src/description.test.ts @@ -38,8 +38,9 @@ const SLACK = IntegrationSlug.make("slack"); const TEMPLATE = AuthTemplateSlug.make("apiKey"); // The execute description lists the top-level integrations the user has -// connected: one bare line per integration slug, deduped across connections, -// names only (no per-integration descriptions). +// connected: one line per integration slug, deduped across connections, with +// the integration's capability description when the catalog carries a real one +// (legacy slug/name-only descriptions are suppressed). const githubPlugin = definePlugin(() => ({ id: "github-plugin" as const, credentialProviders: [memoryProvider()], @@ -118,7 +119,7 @@ describe("buildExecuteDescription", () => { }), ); - it.effect("lists integration names only, with no descriptions", () => + it.effect("renders capability descriptions, suppressing slug/name repeats", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [slackPlugin, githubPlugin] as const }), @@ -142,12 +143,10 @@ describe("buildExecuteDescription", () => { const description = yield* buildExecuteDescription(executor); - // Bare slugs, sorted, with no per-integration description riding the line - // (Slack's "Send and read workspace messages." is dropped). - expect(description).toContain("- `github`"); - expect(description).toContain("- `slack`"); - expect(description).not.toContain("Send and read workspace messages"); - expect(description).not.toContain("- `slack` —"); + // Slack's real capability description rides its line; github's legacy + // description ("GitHub", a name repeat) is suppressed. + expect(description).toContain("- `slack` — Send and read workspace messages."); + expect(description).toContain("- `github`\n"); expect(description).not.toContain("- `github` —"); }), ); @@ -179,6 +178,45 @@ describe("buildExecuteDescription", () => { }), ); + it.effect("truncates long descriptions to one scannable line", () => + Effect.gen(function* () { + const verbosePlugin = definePlugin(() => ({ + id: "verbose-plugin" as const, + credentialProviders: [memoryProvider()], + storage: () => ({}), + extension: (ctx) => ({ + seed: () => + ctx.core.integrations.register({ + slug: IntegrationSlug.make("verbose"), + name: "Verbose", + description: `${"word ".repeat(60).trim()} trailing\nsecond line ignored`, + config: {}, + }), + }), + }))(); + const executor = yield* createExecutor( + makeTestConfig({ plugins: [verbosePlugin] as const }), + ); + yield* executor["verbose-plugin"].seed(); + yield* executor.connections.create({ + owner: "org", + name: ConnectionName.make("main"), + integration: IntegrationSlug.make("verbose"), + template: TEMPLATE, + value: "verbose-token", + }); + + const description = yield* buildExecuteDescription(executor); + const line = description.split("\n").find((l) => l.startsWith("- `verbose`")) ?? ""; + + expect(line.startsWith("- `verbose` — word")).toBe(true); + expect(line.endsWith("…")).toBe(true); + expect(line.length).toBeLessThan(140); + expect(line).not.toContain("trailing"); + expect(line).not.toContain("second line"); + }), + ); + it.effect("omits the Available integrations section when no connections exist", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [] as const })); @@ -224,7 +262,7 @@ describe("parseIntegrationInventory", () => { expect(parseIntegrationInventory("Execute TypeScript in a sandboxed runtime.")).toEqual([]); }); - it("reads item lines only, not the overflow marker or prose", () => { + it("reads item lines only, not the overflow marker, prose, or descriptions", () => { const description = [ "Execute TypeScript in a sandboxed runtime.", "", @@ -232,7 +270,7 @@ describe("parseIntegrationInventory", () => { "", "Integrations you have connected. Their tools live under `tools..…`.", "- `github`", - "- `google_gmail`", + "- `google_gmail` — search, read, and send mail", "- ... 3 more", ].join("\n"); diff --git a/packages/core/execution/src/description.ts b/packages/core/execution/src/description.ts index dec2876fd1..50337471a6 100644 --- a/packages/core/execution/src/description.ts +++ b/packages/core/execution/src/description.ts @@ -1,5 +1,5 @@ import { Effect } from "effect"; -import type { Connection, Executor } from "@executor-js/sdk/core"; +import type { Connection, Executor, Integration } from "@executor-js/sdk/core"; /** * Builds the `execute` tool description dynamically. @@ -10,7 +10,9 @@ import type { Connection, Executor } from "@executor-js/sdk/core"; * description small) * 2. Available integrations (the live, per-session inventory): the top-level * integration slugs the user has connected, deduped across connections, - * names only. The same block is appended to the `execute` skill content. + * each with its one-line capability description from the integration + * catalog (user-editable via the integrations API). The same block is + * appended to the `execute` skill content. */ /** The header that opens the live integration inventory. Exported so the host @@ -25,13 +27,19 @@ export const buildExecuteDescription = (executor: Executor): Effect.Effect { const lines = [ "Execute TypeScript in a sandboxed runtime.", "", 'Before writing code, call `skills({ name: "execute" })` for the workflow on how to use this tool.', ]; - const inventory = formatIntegrationInventory(connections); + const inventory = formatIntegrationInventory(connections, integrations); if (inventory.length > 0) { lines.push(""); lines.push(inventory); @@ -69,14 +77,19 @@ const connectionPath = (connection: Connection): string => { }; // The live inventory block: the top-level integrations the user has connected, -// one bare line per integration slug (deduped across connections, sorted), no -// per-connection prefixes and no descriptions. Empty string when nothing is -// connected. +// one line per integration slug (deduped across connections, sorted) with its +// capability description when the catalog carries one. No per-connection +// prefixes. Empty string when nothing is connected. const INVENTORY_LIMIT = 50; -/** One inventory line per integration: `` - `slug` ``. Owned here beside the - * formatter below so {@link parseIntegrationInventory} cannot drift from it. */ -const INVENTORY_ITEM_PATTERN = /^- `([^`]+)`$/; +/** Longest rendered capability description. The block is always-loaded prompt + * context, so one scannable line per integration is the budget. */ +const INVENTORY_DESCRIPTION_LIMIT = 120; + +/** One inventory line per integration: `` - `slug` `` or + * `` - `slug` — description ``. Owned here beside the formatter below so + * {@link parseIntegrationInventory} cannot drift from it. */ +const INVENTORY_ITEM_PATTERN = /^- `([^`]+)`(?: — .*)?$/; /** * Recover the integration slugs from a built execute description — the exact @@ -96,17 +109,53 @@ export const parseIntegrationInventory = (description: string): readonly string[ return slugs; }; -const formatIntegrationInventory = (connections: readonly Connection[]): string => { +/** One scannable line: first line of the catalog description, whitespace + * collapsed, capped at {@link INVENTORY_DESCRIPTION_LIMIT} on a word edge. */ +const formatInventoryDescription = (description: string | null | undefined): string => { + if (!description) return ""; + const flat = description.split("\n", 1)[0]!.replace(/\s+/g, " ").trim(); + if (flat.length === 0) return ""; + if (flat.length <= INVENTORY_DESCRIPTION_LIMIT) return flat; + const cut = flat.slice(0, INVENTORY_DESCRIPTION_LIMIT); + const edge = cut.lastIndexOf(" "); + return `${edge > 0 ? cut.slice(0, edge) : cut}…`; +}; + +const formatIntegrationInventory = ( + connections: readonly Connection[], + integrations: readonly Integration[], +): string => { const slugs = [...new Set(connections.map((connection) => String(connection.integration)))].sort( (a, b) => a.localeCompare(b), ); if (slugs.length === 0) return ""; + const descriptions = new Map( + integrations.map((integration) => [ + String(integration.slug), + { + name: integration.name, + summary: formatInventoryDescription(integration.description), + }, + ]), + ); const shown = slugs.slice(0, INVENTORY_LIMIT); const lines = [ INTEGRATION_INVENTORY_HEADER, "", "Integrations you have connected. Their tools live under `tools..…`.", - ...shown.map((slug) => `- \`${slug}\``), + ...shown.map((slug) => { + const entry = descriptions.get(slug); + // Legacy rows store the slug or display name as the description; a + // suffix that only repeats the line's own slug adds nothing. + const summary = + entry && + entry.summary.length > 0 && + entry.summary.toLowerCase() !== slug.toLowerCase() && + entry.summary.toLowerCase() !== entry.name.toLowerCase() + ? entry.summary + : ""; + return summary ? `- \`${slug}\` — ${summary}` : `- \`${slug}\``; + }), ]; if (slugs.length > shown.length) { lines.push(`- ... ${slugs.length - shown.length} more`); From 7fffabf279c84db1750f23ef3fdeb5c56f0173ce Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Sat, 26 Sep 2026 04:46:38 +0000 Subject: [PATCH 2/4] Format description.test.ts with oxfmt --- packages/core/execution/src/description.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/execution/src/description.test.ts b/packages/core/execution/src/description.test.ts index 80066e4a7e..f9ad767feb 100644 --- a/packages/core/execution/src/description.test.ts +++ b/packages/core/execution/src/description.test.ts @@ -194,9 +194,7 @@ describe("buildExecuteDescription", () => { }), }), }))(); - const executor = yield* createExecutor( - makeTestConfig({ plugins: [verbosePlugin] as const }), - ); + const executor = yield* createExecutor(makeTestConfig({ plugins: [verbosePlugin] as const })); yield* executor["verbose-plugin"].seed(); yield* executor.connections.create({ owner: "org", From b796660002a0ed84dbac10bb6d5f0ac3b743d32d Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:30:05 +0000 Subject: [PATCH 3/4] fix: deliver tool attachments automatically through MCP --- .../execution/src/attachment-delivery.test.ts | 119 ++++++++++++++++++ .../core/execution/src/attachment-delivery.ts | 108 ++++++++++++++++ packages/core/execution/src/engine.test.ts | 50 ++++++++ packages/core/execution/src/engine.ts | 9 +- packages/core/execution/src/skills.ts | 6 +- 5 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 packages/core/execution/src/attachment-delivery.test.ts create mode 100644 packages/core/execution/src/attachment-delivery.ts diff --git a/packages/core/execution/src/attachment-delivery.test.ts b/packages/core/execution/src/attachment-delivery.test.ts new file mode 100644 index 0000000000..79354c1174 --- /dev/null +++ b/packages/core/execution/src/attachment-delivery.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Effect } from "effect"; +import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; +import { withAttachmentDelivery } from "./attachment-delivery"; + +const image = { type: "image", mimeType: "image/gif", data: "R0lGODlh" }; +const video = { + type: "resource", + resource: { uri: "https://slides.example/slides.mp4", mimeType: "video/mp4", blob: "AAAA" }, +}; + +const run = (value: unknown, code: string) => { + const delivery = withAttachmentDelivery({ invoke: () => Effect.succeed(value) }); + return makeQuickJsExecutor().execute(code, delivery.invoker).pipe(Effect.map(delivery.finish)); +}; + +describe("native attachment delivery", () => { + it.effect("delivers every MCP attachment without emit or a download", () => + Effect.gen(function* () { + const result = yield* run( + { ok: true, data: { content: [image, video], structuredContent: { render_id: "abc" } } }, + "return await tools.slides.org.main.render({});", + ); + expect(result.output).toEqual([ + { type: "content", content: image }, + { type: "content", content: video }, + ]); + expect(JSON.stringify(result.result)).not.toContain(image.data); + expect(JSON.stringify(result.result)).not.toContain(video.resource.blob); + expect(result.result).toMatchObject({ + ok: true, + data: { structuredContent: { render_id: "abc" } }, + }); + }), + ); + + it.effect("delivers files even when the script returns only metadata", () => + Effect.gen(function* () { + const file = { + _tag: "ToolFile", + encoding: "base64", + name: "report.pdf", + mimeType: "application/pdf", + data: "JVBERg==", + byteLength: 4, + }; + const result = yield* run( + { ok: true, data: file }, + "await tools.reports.org.main.get({}); return {done:true};", + ); + expect(result.output).toEqual([{ type: "file", file }]); + expect(result.result).toEqual({ done: true }); + }), + ); + + it.effect("preserves explicit output order without duplicate attachments", () => + Effect.gen(function* () { + const result = yield* run( + { ok: true, data: { content: [image] } }, + 'const r = await tools.slides.org.main.render({}); emit({type:"text",text:"caption"}); emit(r.data.content[0]); return r;', + ); + expect(result.output).toEqual([ + { type: "content", content: { type: "text", text: "caption" } }, + { type: "content", content: image }, + ]); + }), + ); + + it.effect("keeps attachment bytes available for tool-to-tool uploads", () => + Effect.gen(function* () { + const result = yield* run( + { ok: true, data: { content: [image] } }, + "const r = await tools.slides.org.main.render({}); return {bytesAvailable: r.data.content[0].data === 'R0lGODlh'};", + ); + expect(result.result).toEqual({ bytesAvailable: true }); + }), + ); + + it.effect("does not deliver failed tool results", () => + Effect.gen(function* () { + for (const value of [ + { ok: false, error: { content: [image] } }, + { ok: true, data: { isError: true, content: [image] } }, + ]) { + const result = yield* run(value, "return await tools.slides.org.main.render({});"); + expect(result.output ?? []).toEqual([]); + expect(JSON.stringify(result.result)).not.toContain(image.data); + } + }), + ); + + it.effect("keeps successful attachments when a later script step fails", () => + Effect.gen(function* () { + const result = yield* run( + { ok: true, data: { content: [image] } }, + "await tools.slides.org.main.render({}); throw new Error('later');", + ); + expect(result.error).toContain("later"); + expect(result.output).toEqual([{ type: "content", content: image }]); + }), + ); + + it.effect("preserves ordinary text and resource links", () => + Effect.gen(function* () { + const value = { + ok: true, + data: { + content: [ + { type: "text", text: "hello" }, + { type: "resource_link", uri: "https://example.com", name: "report" }, + ], + }, + }; + const result = yield* run(value, "return await tools.slides.org.main.render({});"); + expect(result.result).toEqual(value); + expect(result.output).toBeUndefined(); + }), + ); +}); diff --git a/packages/core/execution/src/attachment-delivery.ts b/packages/core/execution/src/attachment-delivery.ts new file mode 100644 index 0000000000..f3eceb2843 --- /dev/null +++ b/packages/core/execution/src/attachment-delivery.ts @@ -0,0 +1,108 @@ +import { Effect } from "effect"; +import { isToolFile } from "@executor-js/sdk"; +import type { ExecuteResult, SandboxToolInvoker } from "@executor-js/codemode-core"; + +type Output = NonNullable[number]; +const isRecord = (value: unknown): value is Record => + value !== null && typeof value === "object" && !Array.isArray(value); + +/** Keep attachment bytes on the host and expose metadata in the result preview. */ +export const withAttachmentDelivery = (invoker: SandboxToolInvoker) => { + const attachments: Output[] = []; + const seen = new Set(); + + const capture = (value: unknown, deliver: boolean): unknown => { + if (isToolFile(value)) { + const key = `${value.mimeType}:${value.data}`; + if (deliver && !seen.has(key)) { + seen.add(key); + attachments.push({ type: "file", file: value }); + } + return { + name: value.name, + mimeType: value.mimeType, + byteLength: value.byteLength, + delivery: deliver ? "attachment" : "omitted", + }; + } + if (Array.isArray(value)) return value.map((item) => capture(item, deliver)); + if (!isRecord(value)) return value; + // Failed calls must never deliver attachments, including nested MCP errors. + deliver = deliver && value.ok !== false && value.isError !== true; + + const binary = + (value.type === "image" || value.type === "audio") && + typeof value.data === "string" && + typeof value.mimeType === "string" + ? { mimeType: value.mimeType, data: value.data } + : value.type === "resource" && + isRecord(value.resource) && + typeof value.resource.blob === "string" + ? { + mimeType: value.resource.mimeType, + data: value.resource.blob, + uri: value.resource.uri, + } + : undefined; + if (binary) { + const key = `${binary.mimeType}:${binary.data}`; + if (deliver && !seen.has(key)) { + seen.add(key); + attachments.push({ type: "content", content: value }); + } + return { + type: "text", + text: JSON.stringify({ + delivery: deliver ? "attachment" : "omitted", + mimeType: binary.mimeType, + uri: binary.uri, + }), + }; + } + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, capture(item, deliver)]), + ); + }; + + return { + invoker: { + invoke: (input) => + invoker.invoke(input).pipe( + Effect.tap((value) => + Effect.sync(() => { + capture(value, true); + }), + ), + ), + } satisfies SandboxToolInvoker, + finish: (result: ExecuteResult): ExecuteResult => { + // Returning a file or native block directly also delivers it. Explicit emit + // remains supported; match its bytes to avoid sending the attachment twice. + const compactResult = capture(result.result, true); + const key = (output: Output): string | undefined => { + const value = output.type === "file" ? output.file : output.content; + if (isToolFile(value)) return `${value.mimeType}:${value.data}`; + if (!isRecord(value)) return undefined; + if ((value.type === "image" || value.type === "audio") && typeof value.data === "string") + return `${value.mimeType}:${value.data}`; + if ( + value.type === "resource" && + isRecord(value.resource) && + typeof value.resource.blob === "string" + ) + return `${value.resource.mimeType}:${value.resource.blob}`; + return undefined; + }; + const explicitKeys = new Set(); + const explicit = (result.output ?? []).filter((item) => { + const identity = key(item); + if (identity === undefined) return true; + if (explicitKeys.has(identity)) return false; + explicitKeys.add(identity); + return true; + }); + const output = [...attachments.filter((item) => !explicitKeys.has(key(item)!)), ...explicit]; + return { ...result, result: compactResult, ...(output.length ? { output } : {}) }; + }, + }; +}; diff --git a/packages/core/execution/src/engine.test.ts b/packages/core/execution/src/engine.test.ts index 9cd3609072..526b819266 100644 --- a/packages/core/execution/src/engine.test.ts +++ b/packages/core/execution/src/engine.test.ts @@ -44,6 +44,56 @@ const emptyPlugin = definePlugin(() => ({ const makeExecutor = () => createExecutor(makeTestConfig({ plugins: [emptyPlugin()] as const })); +describe("automatic attachment delivery through the execution engine", () => { + for (const mode of ["inline", "pausable", "operator"] as const) { + it.effect(`delivers native content in ${mode} mode without emit`, () => + Effect.gen(function* () { + const image = { type: "image", mimeType: "image/gif", data: "R0lGODlh" }; + const plugin = definePlugin(() => ({ + id: "attachment-test" as const, + storage: () => ({}), + staticIntegrations: () => [ + { + id: "delivery.files", + kind: "in-memory" as const, + name: "Files", + tools: [ + tool({ + name: "render", + description: "Render an attachment.", + inputSchema: Schema.toStandardSchemaV1( + Schema.toStandardJSONSchemaV1(Schema.Struct({})), + ), + execute: () => Effect.succeed({ content: [image] }), + }), + ], + }, + ], + })); + const executor = yield* createExecutor(makeTestConfig({ plugins: [plugin()] as const })); + const engine = createExecutionEngine({ executor, codeExecutor: makeQuickJsExecutor() }); + yield* Effect.addFinalizer(() => + engine.shutdown.pipe(Effect.andThen(executor.close()), Effect.ignore), + ); + const code = "return await tools.delivery.files.render({});"; + const result = + mode === "inline" + ? yield* engine.execute(code, { + onElicitation: () => Effect.succeed({ action: "accept" }), + }) + : yield* engine.executeWithPause(code, { autoApprove: mode === "operator" }); + const completed = + "status" in result && result.status === "completed" ? result.result : result; + expect(completed).toMatchObject({ output: [{ type: "content", content: image }] }); + expect(JSON.stringify(completed)).toContain("attachment"); + expect(JSON.stringify("result" in completed ? completed.result : null)).not.toContain( + image.data, + ); + }).pipe(Effect.scoped), + ); + } +}); + describe("executeWithPause failure propagation", () => { it.effect("surfaces a fast codeExecutor failure as an Exit.Failure", () => Effect.gen(function* () { diff --git a/packages/core/execution/src/engine.ts b/packages/core/execution/src/engine.ts index dad73e2025..64425981c4 100644 --- a/packages/core/execution/src/engine.ts +++ b/packages/core/execution/src/engine.ts @@ -27,6 +27,7 @@ import { } from "./tool-invoker"; import { ExecutionToolError } from "./errors"; import { buildExecuteDescription } from "./description"; +import { withAttachmentDelivery } from "./attachment-delivery"; // --------------------------------------------------------------------------- // Types @@ -727,8 +728,10 @@ export const createExecutionEngine = toolPaths.push(path), ); + const delivery = withAttachmentDelivery(invoker); fiber = yield* Effect.forkDetach( - codeExecutor.execute(code, invoker).pipe( + codeExecutor.execute(code, delivery.invoker).pipe( + Effect.map(delivery.finish), Effect.map((result) => (toolPaths.length === 0 ? result : { ...result, toolPaths })), Effect.withSpan("executor.code.exec"), ), @@ -866,7 +869,9 @@ export const createExecutionEngine = toolPaths.push(path), ); - const result = yield* codeExecutor.execute(code, invoker).pipe( + const delivery = withAttachmentDelivery(invoker); + const result = yield* codeExecutor.execute(code, delivery.invoker).pipe( + Effect.map(delivery.finish), Effect.map((result) => (toolPaths.length === 0 ? result : { ...result, toolPaths })), Effect.withSpan("executor.code.exec"), ); diff --git a/packages/core/execution/src/skills.ts b/packages/core/execution/src/skills.ts index 9bf8f43ef0..589fae046b 100644 --- a/packages/core/execution/src/skills.ts +++ b/packages/core/execution/src/skills.ts @@ -47,12 +47,12 @@ const EXECUTE_SKILL_BODY = [ "- Tool calls return a value union: `{ ok: true, data }` for success or `{ ok: false, error: { code, message, status?, details?, retryable? } }` for expected tool/domain failures. Branch on `result.ok`.", "- `data` is the upstream payload itself. HTTP-backed tools (OpenAPI) also set `http: { status, headers }` beside `data` — read `result.http?.headers` for pagination (Link) or rate-limit headers.", "- Use `emit(value)` to append user-visible output. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitting and returning compose: emitted items come first in the tool result, the returned value follows, and the envelope reports an `emitted` count so you can confirm the items landed.", - '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. A "file-returning tool" includes APIs that return file bytes inside a JSON field, such as Base64-encoded `content`; wrap those payloads in a `ToolFile` and `emit()` them. Emit any attachment with `emit(result.data)`.', + '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. A "file-returning tool" includes APIs that return file bytes inside a JSON field, such as Base64-encoded `content`; wrap those payloads in a `ToolFile` and `emit()` them. Attachments are delivered automatically; no download or `emit()` is needed.', "- Never decode or transcode bytes yourself — the sandbox has no `Buffer`, `atob`, `btoa`, `TextDecoder`, or `TextEncoder`. For base64-encoded bytes in a JSON field, wrap the payload in a `ToolFile` with its `mimeType` and `emit()` it; to forward them to an upload tool, pass the `ToolFile`'s base64 `data` as that tool's `bodyBase64` (both sides speak base64, so nothing is decoded).", '- To emit MCP-native content directly, pass an MCP content block to `emit(...)`, such as `{ type: "image", data, mimeType }`, `{ type: "audio", data, mimeType }`, `{ type: "text", text }`, `{ type: "resource", resource }`, or `{ type: "resource_link", uri, name, ... }`.', "- `emit(ToolFile)` is MIME-based: `image/*` becomes MCP image content, `audio/*` becomes MCP audio content, text-like files become decoded text, and other binary files become embedded MCP resources.", - "- `return` is only for ordinary structured data. Returning a `ToolFile`, a `ToolResult`, an MCP content block, or a bare base64 string does not emit content to the MCP client.", - "- Some providers, including Gmail, return attachment bytes as a `ToolFile` with no public URL to hand off — the bytes themselves are the payload, so `emit(result.data)` to display it, or pass its base64 `data` as another tool's `bodyBase64` to forward it.", + "- Successful tool results containing `ToolFile` values or native MCP image, audio, or embedded binary resources are delivered automatically. Returned previews contain compact metadata instead of file bytes. Explicit `emit()` remains supported and attachments are deduplicated. A bare base64 string is ordinary data.", + "- Some providers, including Gmail, return attachment bytes as a `ToolFile` with no public URL to hand off — the bytes themselves are the payload. Executor delivers the attachment automatically; pass its base64 `data` as another tool's `bodyBase64` to forward it.", "- If `tools.search()` returns `hasMore: true` and you didn't find what you need, fetch the next page: `tools.search({ query, offset: nextOffset, limit })`.", "- Always use the full address when calling tools: `tools....(args)`. The `path` returned by `tools.search()` / `tools.describe.tool()` is already the exact path under `tools` — call `tools[path]` rather than guessing segments.", "- The `tools` object is a lazy proxy — enumerating it (`Object.keys(tools)`, spread, `for...in`) throws. Use `tools.search()` or `tools.executor.coreTools.connections.list({})` instead.", From c377b09d874a6f89e6f6200323b291e24d8930a9 Mon Sep 17 00:00:00 2001 From: Justin Carlson <40642470+justcarlson@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:32:26 +0000 Subject: [PATCH 4/4] fix: omit serialized attachment bytes from result previews --- .../execution/src/attachment-delivery.test.ts | 14 ++++++ .../core/execution/src/attachment-delivery.ts | 45 +++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/core/execution/src/attachment-delivery.test.ts b/packages/core/execution/src/attachment-delivery.test.ts index 79354c1174..21f428593d 100644 --- a/packages/core/execution/src/attachment-delivery.test.ts +++ b/packages/core/execution/src/attachment-delivery.test.ts @@ -15,6 +15,20 @@ const run = (value: unknown, code: string) => { }; describe("native attachment delivery", () => { + it.effect("removes serialized attachment bytes from returns, logs, and emitted text", () => + Effect.gen(function* () { + const media = { ...image, data: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" }; + const result = yield* run( + { ok: true, data: { content: [media] } }, + 'const r = await tools.slides.org.main.render({}); const text = JSON.stringify(r); console.log(text); emit({type:"text",text}); return text;', + ); + expect(JSON.stringify({ result: result.result, logs: result.logs })).not.toContain( + media.data, + ); + expect(result.output?.[0]).toEqual({ type: "content", content: media }); + expect(JSON.stringify(result.output?.[1])).not.toContain(media.data); + }), + ); it.effect("delivers every MCP attachment without emit or a download", () => Effect.gen(function* () { const result = yield* run( diff --git a/packages/core/execution/src/attachment-delivery.ts b/packages/core/execution/src/attachment-delivery.ts index f3eceb2843..78c0e38e79 100644 --- a/packages/core/execution/src/attachment-delivery.ts +++ b/packages/core/execution/src/attachment-delivery.ts @@ -10,9 +10,18 @@ const isRecord = (value: unknown): value is Record => export const withAttachmentDelivery = (invoker: SandboxToolInvoker) => { const attachments: Output[] = []; const seen = new Set(); + const bytes = new Set(); + + const redact = (text: string): string => { + for (const encoded of bytes) { + if (encoded.length >= 16) text = text.replaceAll(encoded, "[attachment bytes omitted]"); + } + return text; + }; const capture = (value: unknown, deliver: boolean): unknown => { if (isToolFile(value)) { + bytes.add(value.data); const key = `${value.mimeType}:${value.data}`; if (deliver && !seen.has(key)) { seen.add(key); @@ -26,7 +35,7 @@ export const withAttachmentDelivery = (invoker: SandboxToolInvoker) => { }; } if (Array.isArray(value)) return value.map((item) => capture(item, deliver)); - if (!isRecord(value)) return value; + if (!isRecord(value)) return typeof value === "string" ? redact(value) : value; // Failed calls must never deliver attachments, including nested MCP errors. deliver = deliver && value.ok !== false && value.isError !== true; @@ -45,6 +54,7 @@ export const withAttachmentDelivery = (invoker: SandboxToolInvoker) => { } : undefined; if (binary) { + bytes.add(binary.data); const key = `${binary.mimeType}:${binary.data}`; if (deliver && !seen.has(key)) { seen.add(key); @@ -94,15 +104,32 @@ export const withAttachmentDelivery = (invoker: SandboxToolInvoker) => { return undefined; }; const explicitKeys = new Set(); - const explicit = (result.output ?? []).filter((item) => { - const identity = key(item); - if (identity === undefined) return true; - if (explicitKeys.has(identity)) return false; - explicitKeys.add(identity); - return true; - }); + const explicit = (result.output ?? []) + .map((item) => { + if ( + item.type === "content" && + isRecord(item.content) && + item.content.type === "text" && + typeof item.content.text === "string" + ) { + return { ...item, content: { ...item.content, text: redact(item.content.text) } }; + } + return item; + }) + .filter((item) => { + const identity = key(item); + if (identity === undefined) return true; + if (explicitKeys.has(identity)) return false; + explicitKeys.add(identity); + return true; + }); const output = [...attachments.filter((item) => !explicitKeys.has(key(item)!)), ...explicit]; - return { ...result, result: compactResult, ...(output.length ? { output } : {}) }; + return { + ...result, + result: compactResult, + ...(result.logs ? { logs: result.logs.map(redact) } : {}), + ...(output.length ? { output } : {}), + }; }, }; };