fix(openapi): put the file-emit contract in file-returning tools' descriptions#1081
Conversation
…criptions A tool whose output is a ToolFile declared the envelope in its output schema, but the output schema is dropped from the hot tool-list projection, so it never reaches the sandbox search step that an agent always walks before its first call. Agents only discovered emit(result.data) after a failed first attempt, or by reading describe.tool. Now any OpenAPI operation whose response is a file gets a short, copyable emit instruction appended to its stored description. Stored descriptions ride both search and describe.tool, so the contract is in front of the agent before it calls the tool, with no new columns and no list-path cost. Non-file tools are untouched. Covered by the tool-descriptions e2e scenario: a PDF-returning operation asserts the hint appears in both the tools.list catalog entry and the tools.schema view.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 6311c7e | Jun 22 2026, 11:33 PM |
Greptile SummaryThis PR appends a short emit-contract hint to the stored description of any OpenAPI tool whose response is a
Confidence Score: 4/5Safe to merge. The core logic change is a straightforward refactor of an existing boolean check, and the new hint is a static string appended only to file-returning tools' descriptions. The description hint in FILE_OUTPUT_HINT omits the optional packages/plugins/openapi/src/sdk/backing.ts — the FILE_OUTPUT_HINT constant omits the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[openApiToolDefsFromCompiled] --> B{returnsFile?}
B -- yes --> C[withFileEmitHint\nappends FILE_OUTPUT_HINT\nto description]
B -- no --> D[descriptionFor unchanged]
B -- yes --> E[outputSchema = ToolFileJsonSchema]
B -- no --> F[outputSchema = normalizeOpenApiRefs\noutputSchema]
C --> G[ToolDef with hint\nin stored description]
D --> H[ToolDef with plain\ndescription]
G --> I[tools.search / tools.list\nalways walks stored description]
G --> J[describe.tool\nalso carries hint]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[openApiToolDefsFromCompiled] --> B{returnsFile?}
B -- yes --> C[withFileEmitHint\nappends FILE_OUTPUT_HINT\nto description]
B -- no --> D[descriptionFor unchanged]
B -- yes --> E[outputSchema = ToolFileJsonSchema]
B -- no --> F[outputSchema = normalizeOpenApiRefs\noutputSchema]
C --> G[ToolDef with hint\nin stored description]
D --> H[ToolDef with plain\ndescription]
G --> I[tools.search / tools.list\nalways walks stored description]
G --> J[describe.tool\nalso carries hint]
Reviews (1): Last reviewed commit: "fix(openapi): put the file-emit contract..." | Re-trigger Greptile |
| const FILE_OUTPUT_HINT = | ||
| 'Returns a ToolFile: the file bytes already decoded into { _tag: "ToolFile", mimeType, encoding, data, byteLength }. ' + | ||
| "To display or forward it, pass the result's data straight to emit(result.data). " + | ||
| "Do not rebuild the envelope or read upstream fields like size."; |
There was a problem hiding this comment.
The
ToolFile schema (defined in tool-result.ts) includes an optional name field for the original filename — name: Schema.optional(Schema.String) — but FILE_OUTPUT_HINT lists only { _tag, mimeType, encoding, data, byteLength }. A model that wants to display or forward the filename alongside the bytes would not know this field exists, and might try to read a non-existent filename field instead. Adding name? to the inline shape description keeps the hint self-consistent with the actual schema.
| const FILE_OUTPUT_HINT = | |
| 'Returns a ToolFile: the file bytes already decoded into { _tag: "ToolFile", mimeType, encoding, data, byteLength }. ' + | |
| "To display or forward it, pass the result's data straight to emit(result.data). " + | |
| "Do not rebuild the envelope or read upstream fields like size."; | |
| const FILE_OUTPUT_HINT = | |
| 'Returns a ToolFile: the file bytes already decoded into { _tag: "ToolFile", name?, mimeType, encoding, data, byteLength }. ' + | |
| "To display or forward it, pass the result's data straight to emit(result.data). " + | |
| "Do not rebuild the envelope or read upstream fields like size."; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | 6311c7e | Commit Preview URL Branch Preview URL |
Jun 22 2026, 11:33 PM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Problem
When an agent loads a file through a file-returning tool (e.g. a Gmail attachment, a PDF invoice), the correct move is to pass the result's
datastraight toemit(result.data). The tool's output schema already declares theToolFileenvelope, so the contract was technically discoverable.But the output schema is dropped from the hot tool-list projection for performance, so it never reaches the sandbox
searchstep.searchis the step a model always walks before its first call. So the model couldn't see the contract until it had already guessed wrong: a typical first attempt hand-rebuilds the envelope (reading a non-existentsizefield, double-converting base64), which silently renders nothing even though the emit "succeeded". The contract only showed up indescribe.tool, which an agent often skips.Fix
Append a short, copyable emit instruction to the stored description of any OpenAPI operation whose response is a file. Stored descriptions ride both
search(always walked) anddescribe.tool, so the contract is in front of the agent before the first call, with no new columns and no list-path cost. This is prevention (right on the first attempt), not recovery.openApiToolDefsFromCompiledis the single producer of a file-returning tool's description, so the hint is computed once from the sameresponseBody.fileHintthat already selects theToolFileoutput schema. Non-file tools are untouched.The appended text:
Coverage
Extends the
tool-descriptionse2e scenario with a PDF-returning operation and asserts the hint appears in both thetools.listcatalog entry and thetools.schemaview, while a non-file tool's description stays clean. The scenario'sdescriptions.mdartifact shows the hint riding the catalog description.Verified on the selfhost target; openapi unit-level file-hint tests still green; format/lint/typecheck clean.