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 }