64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
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
|
|
}
|