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..21f428593d --- /dev/null +++ b/packages/core/execution/src/attachment-delivery.test.ts @@ -0,0 +1,133 @@ +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("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( + { 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..78c0e38e79 --- /dev/null +++ b/packages/core/execution/src/attachment-delivery.ts @@ -0,0 +1,135 @@ +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 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); + 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 typeof value === "string" ? redact(value) : 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) { + bytes.add(binary.data); + 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 ?? []) + .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, + ...(result.logs ? { logs: result.logs.map(redact) } : {}), + ...(output.length ? { output } : {}), + }; + }, + }; +}; diff --git a/packages/core/execution/src/description.test.ts b/packages/core/execution/src/description.test.ts index 365f115585..f9ad767feb 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,43 @@ 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 +260,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 +268,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`); 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.",