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