@@ -2,16 +2,15 @@ import {
22 ErrorCodes ,
33 KimiError ,
44 isKimiError ,
5- makeErrorPayload ,
65 toKimiErrorPayload ,
76} from '#/errors' ;
87import {
98 APIEmptyResponseError ,
109 isRetryableGenerateError ,
1110 type GenerateResult ,
12- type Message ,
1311 type TokenUsage ,
1412 APIContextOverflowError ,
13+ createUserMessage ,
1514} from '@moonshot-ai/kosong' ;
1615
1716import type { Agent } from '..' ;
@@ -30,7 +29,6 @@ import {
3029 resolveCompletionBudget ,
3130} from '../../utils/completion-budget' ;
3231import compactionInstructionTemplate from './compaction-instruction.md?raw' ;
33- import { renderMessagesToText } from './render-messages' ;
3432import { renderTodoList , type TodoItem } from '../../tools/builtin/state/todo-list' ;
3533import type { CompactionBeginData , CompactionResult } from './types' ;
3634import {
@@ -39,12 +37,6 @@ import {
3937 type CompactionStrategy ,
4038} from './strategy' ;
4139
42- type CompactionTelemetryTrigger = CompactionBeginData [ 'source' ] | 'manual-with-prompt' | 'unknown' ;
43-
44- export interface CompactedHistory {
45- text : string ;
46- }
47-
4840export const MAX_COMPACTION_RETRY_ATTEMPTS = 5 ;
4941
5042class CompactionTruncatedError extends Error {
@@ -58,12 +50,9 @@ export class FullCompaction {
5850 protected compactionCountInTurn = 0 ;
5951 protected compacting : {
6052 abortController : AbortController ;
61- startedAt : number ;
62- telemetryTrigger : CompactionTelemetryTrigger ;
6353 promise : Promise < void > ;
6454 blockedByTurn : boolean ;
6555 } | null = null ;
66- protected _compactedHistory : CompactedHistory [ ] = [ ] ;
6756 protected readonly strategy : CompactionStrategy ;
6857
6958 constructor (
@@ -87,10 +76,6 @@ export class FullCompaction {
8776 return this . compacting !== null ;
8877 }
8978
90- get compactedHistory ( ) : readonly CompactedHistory [ ] {
91- return this . _compactedHistory ;
92- }
93-
9479 begin ( data : Readonly < CompactionBeginData > ) : void {
9580 if ( this . compacting ) return ;
9681 if ( data . source === 'manual' ) {
@@ -114,35 +99,20 @@ export class FullCompaction {
11499 type : 'full_compaction.begin' ,
115100 ...data ,
116101 } ) ;
117- this . startCompactionWorker ( data , compactedCount ) ;
118- }
119-
120- private startCompactionWorker (
121- data : Readonly < CompactionBeginData > ,
122- compactedCount : number ,
123- ) : void {
124- const abortController = new AbortController ( ) ;
125102 this . agent . emitEvent ( {
126103 type : 'compaction.started' ,
127104 trigger : data . source ,
128105 instruction : data . instruction ,
129106 } ) ;
130- const active = {
107+ const abortController = new AbortController ( ) ;
108+ this . compacting = {
131109 abortController,
132- startedAt : Date . now ( ) ,
133- telemetryTrigger : compactionTelemetryTrigger ( data . source , data . instruction ) ,
134- promise : Promise . resolve ( ) ,
110+ promise : this . compactionWorker ( abortController . signal , data , compactedCount ) ,
135111 blockedByTurn : false ,
136112 } ;
137- this . compacting = active ;
138- active . promise = this . compactionWorker ( abortController . signal , data , compactedCount ) ;
139113 }
140114
141115 cancel ( ) : void {
142- this . markCanceled ( ) ;
143- }
144-
145- private markCanceled ( ) : void {
146116 this . agent . replayBuilder . patchLast ( 'compaction' , {
147117 result : 'cancelled' ,
148118 } ) ;
@@ -160,9 +130,6 @@ export class FullCompaction {
160130 type : 'full_compaction.complete' ,
161131 } ) ;
162132 this . compacting = null ;
163- this . _compactedHistory . push ( {
164- text : renderMessagesToText ( this . agent . context . history ) ,
165- } ) ;
166133 }
167134
168135 private get tokenCountWithPending ( ) : number {
@@ -197,7 +164,6 @@ export class FullCompaction {
197164 private checkAutoCompaction ( throwOnLimit : boolean = true ) : boolean {
198165 if ( this . compacting ) return true ;
199166 if ( ! this . strategy . shouldCompact ( this . tokenCountWithPending ) ) return false ;
200-
201167 return this . beginAutoCompaction ( throwOnLimit ) ;
202168 }
203169
@@ -236,8 +202,55 @@ export class FullCompaction {
236202 private async compactionWorker (
237203 signal : AbortSignal ,
238204 data : Readonly < CompactionBeginData > ,
239- initialCompactedCount : number ,
205+ compactedCount : number ,
240206 ) : Promise < void > {
207+ try {
208+ const finalResult = {
209+ summary : '' ,
210+ compactedCount : 1 ,
211+ tokensBefore : 0 ,
212+ tokensAfter : 0 ,
213+ } ;
214+
215+ for ( let round = 1 ; ; round ++ ) {
216+ const result = await this . compactionRound ( round , signal , data , compactedCount ) ;
217+ if ( ! result ) return ;
218+
219+ finalResult . summary = result . summary ;
220+ finalResult . compactedCount += result . compactedCount - 1 ;
221+ finalResult . tokensBefore += result . tokensBefore - finalResult . tokensAfter ;
222+ finalResult . tokensAfter = result . tokensAfter ;
223+
224+ if ( result . tokensBefore - result . tokensAfter < 1024 ) break ;
225+ if ( ! this . strategy . shouldBlock ( result . tokensAfter ) ) break ;
226+ compactedCount = this . strategy . computeCompactCount ( this . agent . context . history , data . source ) ;
227+ if ( compactedCount === 0 ) break ;
228+ }
229+ this . markCompleted ( ) ;
230+ this . agent . emitEvent ( { type : 'compaction.completed' , result : finalResult } ) ;
231+ await this . agent . injection . injectGoal ( ) ;
232+ this . triggerPostCompactHook ( data , finalResult ) ;
233+ } catch ( error ) {
234+ if ( isAbortError ( error ) ) return ;
235+ const blockedByTurn = this . compacting ?. blockedByTurn === true ;
236+ this . cancel ( ) ;
237+ this . agent . log . error ( 'compaction failed' , { error } ) ;
238+ if ( blockedByTurn ) {
239+ throw error ;
240+ }
241+ this . agent . emitEvent ( {
242+ type : 'error' ,
243+ ...toKimiErrorPayload ( error ) ,
244+ } ) ;
245+ }
246+ }
247+
248+ private async compactionRound (
249+ round : number ,
250+ signal : AbortSignal ,
251+ data : Readonly < CompactionBeginData > ,
252+ initialCompactedCount : number ,
253+ ) {
241254 const startedAt = Date . now ( ) ;
242255 const originalHistory = [ ...this . agent . context . history ] ;
243256 const tokensBefore = estimateTokensForMessages ( originalHistory ) ;
@@ -263,16 +276,7 @@ export class FullCompaction {
263276 const messagesToCompact = originalHistory . slice ( 0 , compactedCount ) ;
264277 const messages = [
265278 ...this . agent . context . project ( messagesToCompact ) ,
266- {
267- role : 'user' ,
268- content : [
269- {
270- type : 'text' ,
271- text : COMPACTION_INSTRUCTION ( data . instruction ) ,
272- } ,
273- ] ,
274- toolCalls : [ ] ,
275- } satisfies Message ,
279+ createUserMessage ( renderPrompt ( compactionInstructionTemplate , { customInstruction : data . instruction ?? '' } ) ) ,
276280 ] ;
277281 try {
278282 const response = await this . agent . generate (
@@ -329,55 +333,30 @@ export class FullCompaction {
329333 tokensAfter,
330334 } ;
331335
332- const active = this . compacting ! ;
333336 this . agent . telemetry . track ( 'compaction_finished' , {
334- trigger_type : active . telemetryTrigger ,
335- before_tokens : result . tokensBefore ,
336- after_tokens : result . tokensAfter ,
337- duration_ms : Date . now ( ) - active . startedAt ,
338- compacted_count : result . compactedCount ,
339- retry_count : retryCount ,
337+ tokensBefore : result . tokensBefore ,
338+ tokensAfter : result . tokensAfter ,
339+ duration : Date . now ( ) - startedAt ,
340+ compactedCount : result . compactedCount ,
341+ retryCount ,
342+ round ,
340343 ...usage ,
344+ ...data ,
341345 } ) ;
342- this . markCompleted ( ) ;
343- this . agent . emitEvent ( { type : 'compaction.completed' , result } ) ;
344346 this . agent . context . applyCompaction ( result ) ;
345- // Compaction collapses the prefix into a summary, dropping any goal
346- // reminder that lived there. Re-inject it onto the fresh tail so an active
347- // goal does not silently fall out of context. Append-only; no-op off goal mode.
348- await this . agent . injection . injectGoal ( ) ;
349- this . triggerPostCompactHook ( data , result ) ;
347+ return result ;
350348 } catch ( error ) {
351- if ( ! isAbortError ( error ) ) {
352- const active = this . compacting ;
353- const blockedByTurn = active ?. blockedByTurn === true ;
354- this . agent . log . error ( 'compaction failed' , {
355- code : isKimiError ( error ) ? error . code : undefined ,
356- error,
357- } ) ;
358- this . markCanceled ( ) ;
359- if ( ! blockedByTurn ) {
360- const payload =
361- isKimiError ( error ) && error . code === ErrorCodes . AUTH_LOGIN_REQUIRED
362- ? toKimiErrorPayload ( error )
363- : makeErrorPayload ( ErrorCodes . COMPACTION_FAILED , String ( error ) ) ;
364- this . agent . emitEvent ( {
365- type : 'error' ,
366- ...payload ,
367- } ) ;
368- }
369- this . agent . telemetry . track ( 'compaction_failed' , {
370- trigger_type : compactionTelemetryTrigger ( data . source , data . instruction ) ,
371- before_tokens : tokensBefore ,
372- duration_ms : Date . now ( ) - startedAt ,
373- retry_count : retryCount ,
374- error_type : error instanceof Error ? error . name : 'Unknown' ,
375- } ) ;
376- if ( blockedByTurn ) {
377- if ( isKimiError ( error ) && error . code === ErrorCodes . AUTH_LOGIN_REQUIRED ) throw error ;
378- throw new KimiError ( ErrorCodes . COMPACTION_FAILED , String ( error ) , { cause : error } ) ;
379- }
380- }
349+ if ( isAbortError ( error ) ) return ;
350+ this . agent . telemetry . track ( 'compaction_failed' , {
351+ ...data ,
352+ tokensBefore,
353+ duration : Date . now ( ) - startedAt ,
354+ round,
355+ retryCount,
356+ errorType : error instanceof Error ? error . name : 'Unknown' ,
357+ } ) ;
358+ if ( isKimiError ( error ) && error . code === ErrorCodes . AUTH_LOGIN_REQUIRED ) throw error ;
359+ throw new KimiError ( ErrorCodes . COMPACTION_FAILED , String ( error ) , { cause : error } ) ;
381360 }
382361 }
383362
@@ -435,17 +414,3 @@ function extractCompactionSummary(response: GenerateResult): string {
435414 }
436415 return summary ;
437416}
438-
439- export const COMPACTION_INSTRUCTION = ( customInstruction = '' ) : string =>
440- renderPrompt ( compactionInstructionTemplate , { customInstruction } ) ;
441-
442- function compactionTelemetryTrigger (
443- trigger : CompactionBeginData [ 'source' ] | undefined ,
444- instruction : string | undefined ,
445- ) : CompactionTelemetryTrigger {
446- if ( trigger === undefined ) return 'unknown' ;
447- if ( trigger === 'manual' && instruction !== undefined && instruction . length > 0 ) {
448- return 'manual-with-prompt' ;
449- }
450- return trigger ;
451- }
0 commit comments