(
messages: Message[],
options: {
keepLastTurns?: number;
summarize: (msgs: Message[]) => Promise<string>;
},
)
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Compact a message list by summarizing all but the most recent `keepLast` turns. |
| 136 | * A "turn" is a user message + everything until the next user message. |
| 137 | * |
| 138 | * The caller provides a `summarize` callback that runs an LLM call on the |
| 139 | * messages-to-summarize and returns the summary text. |
| 140 | */ |
| 141 | export async function compactMessages( |
| 142 | messages: Message[], |
| 143 | options: { |
| 144 | keepLastTurns?: number; |
| 145 | summarize: (msgs: Message[]) => Promise<string>; |
| 146 | }, |
| 147 | ): Promise<CompactionResult> { |
| 148 | const keepLastTurns = options.keepLastTurns ?? 6; |
| 149 | const before = estimateTokens(messages); |
| 150 | |
| 151 | const system = messages.filter(m => m.role === 'system'); |
| 152 | const nonSystem = messages.filter(m => m.role !== 'system'); |
| 153 | |
| 154 | // Group into turn-groups: user msg starts each group |
| 155 | const groups: Message[][] = []; |
| 156 | let cur: Message[] = []; |
| 157 | for (const m of nonSystem) { |
| 158 | if (m.role === 'user' && cur.length > 0) { |
| 159 | groups.push(cur); |
| 160 | cur = [m]; |
| 161 | } else { |
| 162 | cur.push(m); |
| 163 | } |
| 164 | } |
| 165 | if (cur.length > 0) groups.push(cur); |
| 166 | |
| 167 | if (groups.length <= keepLastTurns) { |
| 168 | // Nothing to do |
| 169 | return { messages, before, after: before, turnsCompacted: 0 }; |
| 170 | } |
| 171 | |
| 172 | const toSummarize = groups.slice(0, groups.length - keepLastTurns).flat(); |
| 173 | const toKeep = groups.slice(groups.length - keepLastTurns).flat(); |
| 174 | const turnsCompacted = groups.length - keepLastTurns; |
| 175 | |
| 176 | // Filter out existing summary messages from the to-summarize set |
| 177 | // and keep ALL system messages (including any prior summary) so the new |
| 178 | // summary CAN reference them. |
| 179 | const summarizationInput = buildCompactionRequest(toSummarize); |
| 180 | let summaryText: string; |
| 181 | try { |
| 182 | summaryText = await options.summarize(summarizationInput); |
| 183 | } catch (e: any) { |
| 184 | logger.warn('Compaction summarization failed; returning original messages', { err: e?.message }); |
| 185 | return { messages, before, after: before, turnsCompacted: 0 }; |
| 186 | } |
| 187 | // Refuse to trade real history for an empty summary. |
| 188 | // |
| 189 | // Compaction DELETES the summarized turns. A throw is handled above, but a summarizer can |
| 190 | // also succeed and return nothing useful: an aborted stream (the caller's loop does |
| 191 | // `if (signal?.aborted) break` and returns ''), a provider sending an empty body, a local |
no test coverage detected