* Fold content blocks into a tool_result's content. Returns the updated * tool_result, or `null` if smoosh is impossible (tool_reference constraint). * * Valid block types inside tool_result.content per SDK: text, image, * search_result, document. All of these smoosh. tool_reference (beta) canno
( tr: ToolResultBlockParam, blocks: ContentBlockParam[], )
| 2532 | * - otherwise → array, with adjacent text merged (notebook.ts idiom) |
| 2533 | */ |
| 2534 | function smooshIntoToolResult( |
| 2535 | tr: ToolResultBlockParam, |
| 2536 | blocks: ContentBlockParam[], |
| 2537 | ): ToolResultBlockParam | null { |
| 2538 | if (blocks.length === 0) return tr |
| 2539 | |
| 2540 | const existing = tr.content |
| 2541 | if (Array.isArray(existing) && existing.some(isToolReferenceBlock)) { |
| 2542 | return null |
| 2543 | } |
| 2544 | |
| 2545 | // API constraint: is_error tool_results must contain only text blocks. |
| 2546 | // Queued-command siblings can carry images (pasted screenshot) — smooshing |
| 2547 | // those into an error result produces a transcript that 400s on every |
| 2548 | // subsequent call and can't be recovered by /fork. The image isn't lost: |
| 2549 | // it arrives as a proper user turn anyway. |
| 2550 | if (tr.is_error) { |
| 2551 | blocks = blocks.filter(b => b.type === 'text') |
| 2552 | if (blocks.length === 0) return tr |
| 2553 | } |
| 2554 | |
| 2555 | const allText = blocks.every(b => b.type === 'text') |
| 2556 | |
| 2557 | // Preserve string shape when existing was string/undefined and all incoming |
| 2558 | // blocks are text — this is the common case (hook reminders into Bash/Read |
| 2559 | // results) and matches the legacy smoosh output shape. |
| 2560 | if (allText && (existing === undefined || typeof existing === 'string')) { |
| 2561 | const joined = [ |
| 2562 | (existing ?? '').trim(), |
| 2563 | ...blocks.map(b => (b as TextBlockParam).text.trim()), |
| 2564 | ] |
| 2565 | .filter(Boolean) |
| 2566 | .join('\n\n') |
| 2567 | return { ...tr, content: joined } |
| 2568 | } |
| 2569 | |
| 2570 | // General case: normalize to array, concat, merge adjacent text |
| 2571 | const base: ToolResultContentItem[] = |
| 2572 | existing === undefined |
| 2573 | ? [] |
| 2574 | : typeof existing === 'string' |
| 2575 | ? existing.trim() |
| 2576 | ? [{ type: 'text', text: existing.trim() }] |
| 2577 | : [] |
| 2578 | : [...existing] |
| 2579 | |
| 2580 | const merged: ToolResultContentItem[] = [] |
| 2581 | for (const b of [...base, ...blocks]) { |
| 2582 | if (b.type === 'text') { |
| 2583 | const t = b.text.trim() |
| 2584 | if (!t) continue |
| 2585 | const prev = merged.at(-1) |
| 2586 | if (prev?.type === 'text') { |
| 2587 | merged[merged.length - 1] = { ...prev, text: `${prev.text}\n\n${t}` } // lvalue |
| 2588 | } else { |
| 2589 | merged.push({ type: 'text', text: t }) |
| 2590 | } |
| 2591 | } else { |
no test coverage detected