mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: retry empty compaction summaries (#12)
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
This commit is contained in:
parent
15b018fc84
commit
89ea8959eb
3 changed files with 70 additions and 10 deletions
6
.changeset/retry-empty-compaction-summary.md
Normal file
6
.changeset/retry-empty-compaction-summary.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Retry compaction responses that do not contain a summary before updating conversation history.
|
||||
|
|
@ -369,7 +369,7 @@ export class FullCompaction {
|
|||
toolCalls: [],
|
||||
} satisfies Message,
|
||||
];
|
||||
const { response, retryCount } = await this.generateCompactionResponse({
|
||||
const { response, retryCount, summary } = await this.generateCompactionResponse({
|
||||
messages,
|
||||
signal,
|
||||
onRetry: (count) => {
|
||||
|
|
@ -380,13 +380,6 @@ export class FullCompaction {
|
|||
this.agent.usage.record(model, response.usage);
|
||||
}
|
||||
|
||||
const summary =
|
||||
typeof response.message.content === 'string'
|
||||
? response.message.content
|
||||
: response.message.content
|
||||
.map((part) => (part.type === 'text' ? part.text : ''))
|
||||
.join('');
|
||||
|
||||
const newHistory = this.agent.context.history;
|
||||
for (let i = 0; i < originalHistory.length; i++) {
|
||||
if (newHistory[i] !== originalHistory[i]) {
|
||||
|
|
@ -443,7 +436,11 @@ export class FullCompaction {
|
|||
readonly messages: Message[];
|
||||
readonly signal: AbortSignal;
|
||||
readonly onRetry?: ((retryCount: number) => void) | undefined;
|
||||
}): Promise<{ readonly response: GenerateResult; readonly retryCount: number }> {
|
||||
}): Promise<{
|
||||
readonly response: GenerateResult;
|
||||
readonly summary: string;
|
||||
readonly retryCount: number;
|
||||
}> {
|
||||
const maxAttempts =
|
||||
this.agent.providerManager?.config.loopControl?.maxRetriesPerStep ??
|
||||
DEFAULT_MAX_RETRY_ATTEMPTS;
|
||||
|
|
@ -478,7 +475,8 @@ export class FullCompaction {
|
|||
undefined,
|
||||
{ signal },
|
||||
);
|
||||
return { response, retryCount };
|
||||
const summary = extractCompactionSummary(response);
|
||||
return { response, summary, retryCount };
|
||||
} catch (error) {
|
||||
if (attempt >= maxAttempts || !isRetryableCompactionError(error)) {
|
||||
throw error;
|
||||
|
|
@ -530,6 +528,20 @@ export class FullCompaction {
|
|||
}
|
||||
}
|
||||
|
||||
function extractCompactionSummary(response: GenerateResult): string {
|
||||
const summary =
|
||||
typeof response.message.content === 'string'
|
||||
? response.message.content
|
||||
: response.message.content.map((part) => (part.type === 'text' ? part.text : '')).join('');
|
||||
|
||||
if (summary.trim().length === 0) {
|
||||
throw new APIEmptyResponseError(
|
||||
'The compaction response did not contain a non-empty summary.',
|
||||
);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
export const COMPACTION_INSTRUCTION = (customInstruction = ''): string =>
|
||||
renderPrompt(compactionInstructionTemplate, { customInstruction });
|
||||
|
||||
|
|
|
|||
|
|
@ -435,6 +435,48 @@ describe('Agent compaction', () => {
|
|||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
it('retries compaction responses with empty summaries before applying context', async () => {
|
||||
vi.useFakeTimers();
|
||||
const firstEmptySummary = deferred<void>();
|
||||
let attempts = 0;
|
||||
const generate: GenerateFn = async () => {
|
||||
attempts += 1;
|
||||
if (attempts <= 2) {
|
||||
if (attempts === 1) firstEmptySummary.resolve();
|
||||
return textResult(attempts === 1 ? '' : ' \n');
|
||||
}
|
||||
return textResult('Recovered compacted summary.');
|
||||
};
|
||||
const ctx = testAgent({ generate });
|
||||
ctx.configure({
|
||||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await firstEmptySummary.promise;
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await compacted;
|
||||
|
||||
expect(attempts).toBe(3);
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
]);
|
||||
expect(
|
||||
ctx.allEvents.filter((event) => event.event === 'full_compaction.complete'),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
args: expect.objectContaining({ summary: 'Recovered compacted summary.' }),
|
||||
}),
|
||||
]);
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
it('waits before retrying compaction generation after a retryable failure', async () => {
|
||||
vi.useFakeTimers();
|
||||
const firstAttemptFailed = deferred<void>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue