(
result: MCPToolResult,
qualifiedToolName: string,
options: McpOutputOptions = {},
)
| 148 | * wrap when the result is media-only, so the model can attribute binary parts. |
| 149 | */ |
| 150 | export async function mcpResultToExecutableOutput( |
| 151 | result: MCPToolResult, |
| 152 | qualifiedToolName: string, |
| 153 | options: McpOutputOptions = {}, |
| 154 | ): Promise<{ output: string | ContentPart[]; isError: boolean; truncated?: true }> { |
| 155 | const converted: ContentPart[] = []; |
| 156 | for (const block of result.content) { |
| 157 | const part = convertMCPContentBlock(block); |
| 158 | if (part !== null) { |
| 159 | converted.push(part); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | const wrapped = wrapMediaOnly(converted, qualifiedToolName); |
| 164 | // Text budget FIRST, on the tool's own text only: captions inserted by the |
| 165 | // compression step below must never compete with a chatty tool's text for |
| 166 | // the budget — an evicted or mid-string-sliced caption silently |
| 167 | // reintroduces the downsampling this pipeline promises to announce. |
| 168 | const budgeted = applyTextBudget(wrapped); |
| 169 | // Shrink oversized images BEFORE the per-part byte cap, so a large but |
| 170 | // compressible screenshot is downsampled and kept rather than dropped to a |
| 171 | // text notice. Compression is never silent: each re-encoded image gains a |
| 172 | // caption stating what the original was, and the original bytes are |
| 173 | // persisted (best effort, into the session's media-originals dir when |
| 174 | // known) so the model can read detail back via ReadMediaFile + region. |
| 175 | // Parts that cannot be compressed pass through. |
| 176 | const compressed = await compressImageContentParts(budgeted.parts, { |
| 177 | annotate: { |
| 178 | persistOriginal: (bytes, mimeType) => |
| 179 | persistOriginalImage( |
| 180 | bytes, |
| 181 | mimeType, |
| 182 | options.originalsDir === undefined ? {} : { dir: options.originalsDir }, |
| 183 | ), |
| 184 | }, |
| 185 | }); |
| 186 | const capped = applyBinaryPartCap(compressed); |
| 187 | const truncated = budgeted.truncated || capped.truncated; |
| 188 | const output = collapseSingleText(capped.parts); |
| 189 | return truncated |
| 190 | ? { output, isError: result.isError, truncated: true } |
| 191 | : { output, isError: result.isError }; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * If `parts` contains media but no non-empty text, surround it with |
no test coverage detected