* Extract candidate tool_result blocks grouped by API-level user message. * * normalizeMessagesForAPI merges consecutive user messages into one * (Bedrock compat; 1P does the same server-side), so parallel tool * results that arrive as N separate user messages in our state become * ONE user mes
( messages: Message[], )
| 598 | * Only groups with at least one eligible candidate are returned. |
| 599 | */ |
| 600 | function collectCandidatesByMessage( |
| 601 | messages: Message[], |
| 602 | ): ToolResultCandidate[][] { |
| 603 | const groups: ToolResultCandidate[][] = [] |
| 604 | let current: ToolResultCandidate[] = [] |
| 605 | |
| 606 | const flush = () => { |
| 607 | if (current.length > 0) groups.push(current) |
| 608 | current = [] |
| 609 | } |
| 610 | |
| 611 | // Track all assistant message.ids seen so far — same-ID fragments are |
| 612 | // merged by normalizeMessagesForAPI (messages.ts ~2126 walks back PAST |
| 613 | // different-ID assistants via `continue`), so any re-appearance of a |
| 614 | // previously-seen ID must NOT create a group boundary. Two scenarios: |
| 615 | // • Consecutive: streamingToolExecution yields one AssistantMessage per |
| 616 | // content_block_stop (same id); a fast tool drains between blocks; |
| 617 | // abort/hook-stop leaves [asst(X), user(trA), asst(X), user(trB)]. |
| 618 | // • Interleaved: coordinator/teammate streams mix different responses |
| 619 | // so [asst(X), user(trA), asst(Y), user(trB), asst(X), user(trC)]. |
| 620 | // In both, normalizeMessagesForAPI merges the X fragments into one wire |
| 621 | // assistant, and their following tool_results merge into one wire user |
| 622 | // message — so the budget must see them as one group too. |
| 623 | const seenAsstIds = new Set<string>() |
| 624 | for (const message of messages) { |
| 625 | if (message.type === 'user') { |
| 626 | current.push(...collectCandidatesFromMessage(message)) |
| 627 | } else if (message.type === 'assistant') { |
| 628 | if (!seenAsstIds.has(message.message.id)) { |
| 629 | flush() |
| 630 | seenAsstIds.add(message.message.id) |
| 631 | } |
| 632 | } |
| 633 | // progress / attachment / system are filtered or merged by |
| 634 | // normalizeMessagesForAPI — they don't create wire boundaries. |
| 635 | } |
| 636 | flush() |
| 637 | |
| 638 | return groups |
| 639 | } |
| 640 | |
| 641 | /** |
| 642 | * Partition candidates by their prior decision state: |
no test coverage detected