( a: ContentBlockParam[], b: ContentBlockParam[], )
| 2598 | } |
| 2599 | |
| 2600 | export function mergeUserContentBlocks( |
| 2601 | a: ContentBlockParam[], |
| 2602 | b: ContentBlockParam[], |
| 2603 | ): ContentBlockParam[] { |
| 2604 | // See https://anthropic.slack.com/archives/C06FE2FP0Q2/p1747586370117479 and |
| 2605 | // https://anthropic.slack.com/archives/C0AHK9P0129/p1773159663856279: |
| 2606 | // any sibling after tool_result renders as </function_results>\n\nHuman:<...> |
| 2607 | // on the wire. Repeated mid-conversation, this teaches capy to emit Human: at |
| 2608 | // a bare tail → 3-token empty end_turn. A/B (sai-20260310-161901) validated: |
| 2609 | // smoosh into tool_result.content → 92% → 0%. |
| 2610 | const lastBlock = last(a) |
| 2611 | if (lastBlock?.type !== 'tool_result') { |
| 2612 | return [...a, ...b] |
| 2613 | } |
| 2614 | |
| 2615 | if (!checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_chair_sermon')) { |
| 2616 | // Legacy (ungated) smoosh: only string-content tool_result + all-text |
| 2617 | // siblings → joined string. Matches pre-universal-smoosh behavior on main. |
| 2618 | // The precondition guarantees smooshIntoToolResult hits its string path |
| 2619 | // (no tool_reference bail, string output shape preserved). |
| 2620 | if ( |
| 2621 | typeof lastBlock.content === 'string' && |
| 2622 | b.every(x => x.type === 'text') |
| 2623 | ) { |
| 2624 | const copy = a.slice() |
| 2625 | copy[copy.length - 1] = smooshIntoToolResult(lastBlock, b)! |
| 2626 | return copy |
| 2627 | } |
| 2628 | return [...a, ...b] |
| 2629 | } |
| 2630 | |
| 2631 | // Universal smoosh (gated): fold all non-tool_result block types (text, |
| 2632 | // image, document, search_result) into tool_result.content. tool_result |
| 2633 | // blocks stay as siblings (hoisted later by hoistToolResults). |
| 2634 | const toSmoosh = b.filter(x => x.type !== 'tool_result') |
| 2635 | const toolResults = b.filter(x => x.type === 'tool_result') |
| 2636 | if (toSmoosh.length === 0) { |
| 2637 | return [...a, ...b] |
| 2638 | } |
| 2639 | |
| 2640 | const smooshed = smooshIntoToolResult(lastBlock, toSmoosh) |
| 2641 | if (smooshed === null) { |
| 2642 | // tool_reference constraint — fall back to siblings |
| 2643 | return [...a, ...b] |
| 2644 | } |
| 2645 | |
| 2646 | return [...a.slice(0, -1), smooshed, ...toolResults] |
| 2647 | } |
| 2648 | |
| 2649 | // Sometimes the API returns empty messages (eg. "\n\n"). We need to filter these out, |
| 2650 | // otherwise they will give an API error when we send them to the API next time we call query(). |
no test coverage detected