| import type { Message } from '../../types/message.js' | |
| /** | |
| * Groups messages at API-round boundaries: one group per API round-trip. | |
| * A boundary fires when a NEW assistant response begins (different | |
| * message.id from the prior assistant). For well-formed conversations | |
| * this is an API-safe split point β the API contract requires every | |
| * tool_use to be resolved before the next assistant turn, so pairing | |
| * validity falls out of the assistant-id boundary. For malformed inputs | |
| * (dangling tool_use after resume/truncation) the fork's | |
| * ensureToolResultPairing repairs the split at API time. | |
| * | |
| * Replaces the prior human-turn grouping (boundaries only at real user | |
| * prompts) with finer-grained API-round grouping, allowing reactive | |
| * compact to operate on single-prompt agentic sessions (SDK/CCR/eval | |
| * callers) where the entire workload is one human turn. | |
| * | |
| * Extracted to its own file to break the compact.ts β compactMessages.ts | |
| * cycle (CC-1180) β the cycle shifted module-init order enough to surface | |
| * a latent ws CJS/ESM resolution race in CI shard-2. | |
| */ | |
| export function groupMessagesByApiRound(messages: Message[]): Message[][] { | |
| const groups: Message[][] = [] | |
| let current: Message[] = [] | |
| // message.id of the most recently seen assistant. This is the sole | |
| // boundary gate: streaming chunks from the same API response share an | |
| // id, so boundaries only fire at the start of a genuinely new round. | |
| // normalizeMessages yields one AssistantMessage per content block, and | |
| // StreamingToolExecutor interleaves tool_results between chunks live | |
| // (yield order, not concat order β see query.ts:613). The id check | |
| // correctly keeps `[tu_A(id=X), result_A, tu_B(id=X)]` in one group. | |
| let lastAssistantId: string | undefined | |
| // In a well-formed conversation the API contract guarantees every | |
| // tool_use is resolved before the next assistant turn, so lastAssistantId | |
| // alone is a sufficient boundary gate. Tracking unresolved tool_use IDs | |
| // would only do work when the conversation is malformed (dangling tool_use | |
| // after resume-from-partial-batch or max_tokens truncation) β and in that | |
| // case it pins the gate shut forever, merging all subsequent rounds into | |
| // one group. We let those boundaries fire; the summarizer fork's own | |
| // ensureToolResultPairing at claude.ts:1136 repairs the dangling tu at | |
| // API time. | |
| for (const msg of messages) { | |
| if ( | |
| msg.type === 'assistant' && | |
| msg.message.id !== lastAssistantId && | |
| current.length > 0 | |
| ) { | |
| groups.push(current) | |
| current = [msg] | |
| } else { | |
| current.push(msg) | |
| } | |
| if (msg.type === 'assistant') { | |
| lastAssistantId = msg.message.id | |
| } | |
| } | |
| if (current.length > 0) { | |
| groups.push(current) | |
| } | |
| return groups | |
| } | |