MCPcopy
hub / github.com/codeaashu/claude-code / maybePersistLargeToolResult

Function maybePersistLargeToolResult

src/utils/toolResultStorage.ts:272–334  ·  view source on GitHub ↗

* Handle large tool results by persisting to disk instead of truncating. * Returns the original block if no persistence needed, or a modified block * with the content replaced by a reference to the persisted file.

(
  toolResultBlock: ToolResultBlockParam,
  toolName: string,
  persistenceThreshold?: number,
)

Source from the content-addressed store, hash-verified

270 * with the content replaced by a reference to the persisted file.
271 */
272async function maybePersistLargeToolResult(
273 toolResultBlock: ToolResultBlockParam,
274 toolName: string,
275 persistenceThreshold?: number,
276): Promise<ToolResultBlockParam> {
277 // Check size first before doing any async work - most tool results are small
278 const content = toolResultBlock.content
279
280 // inc-4586: Empty tool_result content at the prompt tail causes some models
281 // (notably capybara) to emit the \n\nHuman: stop sequence and end their turn
282 // with zero output. The server renderer inserts no \n\nAssistant: marker after
283 // tool results, so a bare </function_results>\n\n pattern-matches to a turn
284 // boundary. Several tools can legitimately produce empty output (silent-success
285 // shell commands, MCP servers returning content:[], REPL statements, etc.).
286 // Inject a short marker so the model always has something to react to.
287 if (isToolResultContentEmpty(content)) {
288 logEvent('tengu_tool_empty_result', {
289 toolName: sanitizeToolNameForAnalytics(toolName),
290 })
291 return {
292 ...toolResultBlock,
293 content: `(${toolName} completed with no output)`,
294 }
295 }
296 // Narrow after the emptiness guard — content is non-nullish past this point.
297 if (!content) {
298 return toolResultBlock
299 }
300
301 // Skip persistence for image content blocks - they need to be sent as-is to Claude
302 if (hasImageBlock(content)) {
303 return toolResultBlock
304 }
305
306 const size = contentSize(content)
307
308 // Use tool-specific threshold if provided, otherwise fall back to global limit
309 const threshold = persistenceThreshold ?? MAX_TOOL_RESULT_BYTES
310 if (size <= threshold) {
311 return toolResultBlock
312 }
313
314 // Persist the entire content as a unit
315 const result = await persistToolResult(content, toolResultBlock.tool_use_id)
316 if (isPersistError(result)) {
317 // If persistence failed, return the original block unchanged
318 return toolResultBlock
319 }
320
321 const message = buildLargeToolResultMessage(result)
322
323 // Log analytics
324 logEvent('tengu_tool_result_persisted', {
325 toolName: sanitizeToolNameForAnalytics(toolName),
326 originalSizeBytes: result.originalSize,
327 persistedSizeBytes: message.length,
328 estimatedOriginalTokens: Math.ceil(result.originalSize / BYTES_PER_TOKEN),
329 estimatedPersistedTokens: Math.ceil(message.length / BYTES_PER_TOKEN),

Callers 2

processToolResultBlockFunction · 0.85

Calls 8

isToolResultContentEmptyFunction · 0.85
logEventFunction · 0.85
hasImageBlockFunction · 0.85
contentSizeFunction · 0.85
persistToolResultFunction · 0.85
isPersistErrorFunction · 0.85

Tested by

no test coverage detected