mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix(agent-core-v2): align compaction empty retry
This commit is contained in:
parent
61131840c3
commit
b1cdaf4c6c
2 changed files with 63 additions and 25 deletions
|
|
@ -19,6 +19,7 @@ import {
|
|||
APIContextOverflowError,
|
||||
APIEmptyResponseError,
|
||||
APIStatusError,
|
||||
isRetryableGenerateError,
|
||||
} from '#/app/llmProtocol/errors';
|
||||
import { createUserMessage, type Message } from '#/app/llmProtocol/message';
|
||||
import { type TokenUsage } from '#/app/llmProtocol/usage';
|
||||
|
|
@ -396,7 +397,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
let retryCount = 0;
|
||||
|
||||
try {
|
||||
let compactedCount = Math.min(initialCompactedCount, originalHistory.length);
|
||||
const compactedCount = Math.min(initialCompactedCount, originalHistory.length);
|
||||
const signal = active.abortController.signal;
|
||||
signal.throwIfAborted();
|
||||
|
||||
|
|
@ -420,8 +421,11 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
|
||||
const delays = retryBackoffDelays(MAX_COMPACTION_RETRY_ATTEMPTS);
|
||||
let attempt: CompactionAttemptResult | undefined;
|
||||
let historyForModel = originalHistory.slice(0, compactedCount);
|
||||
let droppedCount = 0;
|
||||
let emptyOrTruncatedShrinkCount = 0;
|
||||
while (true) {
|
||||
const messagesToCompact = originalHistory.slice(0, compactedCount);
|
||||
const messagesToCompact = historyForModel;
|
||||
// Raw context slice — `llmRequester` projects every request once;
|
||||
// projecting here too would run micro-compaction on shifted indices.
|
||||
const messages: Message[] = [...messagesToCompact, createUserMessage(instruction)];
|
||||
|
|
@ -434,10 +438,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
maxOutputSize: compactionMaxOutputSize,
|
||||
source: { type: 'operation', requestKind: 'full_compaction' },
|
||||
retry: {
|
||||
maxAttempts: MAX_COMPACTION_RETRY_ATTEMPTS,
|
||||
onRetry: () => {
|
||||
retryCount += 1;
|
||||
},
|
||||
maxAttempts: 1,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
|
|
@ -446,19 +447,33 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (
|
||||
this.shouldRecoverFromCompactionOverflow(error, messages) ||
|
||||
error instanceof CompactionTruncatedError ||
|
||||
error instanceof APIEmptyResponseError
|
||||
) {
|
||||
if (this.shouldRecoverFromCompactionOverflow(error, messages)) {
|
||||
const reduced = this.strategy.reduceCompactOnOverflow(messagesToCompact);
|
||||
// An overflow that cannot shrink further would replay the same
|
||||
// request; give up (v1: throws when the history cannot shrink).
|
||||
if (error instanceof APIContextOverflowError && reduced >= compactedCount) {
|
||||
if (reduced >= messagesToCompact.length) {
|
||||
throw error;
|
||||
}
|
||||
compactedCount = reduced;
|
||||
} else {
|
||||
droppedCount += messagesToCompact.length - reduced;
|
||||
historyForModel = messagesToCompact.slice(0, reduced);
|
||||
retryCount = 0;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(error instanceof CompactionTruncatedError || error instanceof APIEmptyResponseError) &&
|
||||
messagesToCompact.length > 1
|
||||
) {
|
||||
emptyOrTruncatedShrinkCount += 1;
|
||||
if (emptyOrTruncatedShrinkCount > MAX_COMPACTION_RETRY_ATTEMPTS) {
|
||||
throw error;
|
||||
}
|
||||
const reduced = dropOldestMessageAndLeadingToolResults(messagesToCompact);
|
||||
droppedCount += messagesToCompact.length - reduced.length;
|
||||
historyForModel = reduced;
|
||||
retryCount = 0;
|
||||
continue;
|
||||
}
|
||||
if (!isRetryableGenerateError(error)) {
|
||||
throw error;
|
||||
}
|
||||
if (retryCount + 1 >= MAX_COMPACTION_RETRY_ATTEMPTS) {
|
||||
|
|
@ -489,6 +504,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
contextSummary: buildCompactionSummaryText(summary),
|
||||
compactedCount,
|
||||
tokensBefore,
|
||||
droppedCount: droppedCount === 0 ? undefined : droppedCount,
|
||||
});
|
||||
|
||||
this.telemetry.track('compaction_finished', {
|
||||
|
|
@ -596,6 +612,21 @@ function historySafeToCompact(
|
|||
return current.slice(original.length).every(isRealUserInput);
|
||||
}
|
||||
|
||||
function dropOldestMessageAndLeadingToolResults<T extends { readonly role: string }>(
|
||||
messages: readonly T[],
|
||||
): T[] {
|
||||
if (messages.length <= 1) return messages.slice();
|
||||
return dropLeadingToolResults(messages.slice(1));
|
||||
}
|
||||
|
||||
function dropLeadingToolResults<T extends { readonly role: string }>(messages: readonly T[]): T[] {
|
||||
let start = 0;
|
||||
while (start < messages.length && messages[start]!.role === 'tool') {
|
||||
start += 1;
|
||||
}
|
||||
return messages.slice(start);
|
||||
}
|
||||
|
||||
function usageTelemetry(usage: TokenUsage | null): CompactionTelemetryProperties {
|
||||
if (usage === null) return {};
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { makeHookRunner } from '../externalHooks/runner-stub';
|
|||
import type { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner';
|
||||
import { MASTER_ENV } from '#/app/flag/flagService';
|
||||
import { microCompactionFlag } from '#/agent/microCompaction/flag';
|
||||
import { COMPACTION_SUMMARY_PREFIX } from '#/agent/contextMemory/compactionHandoff';
|
||||
import { estimateTokensForMessages } from '#/_base/utils/tokens';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
|
||||
import type { TestAgentContext, TestAgentOptions, TestAgentServiceOverride } from '../harness';
|
||||
|
|
@ -636,23 +637,26 @@ describe('FullCompaction', () => {
|
|||
await completed;
|
||||
|
||||
expect(attempts).toBe(3);
|
||||
// Each empty summary shrinks the compacted prefix before retrying, so the
|
||||
// recovered summary compacts only the older exchange and leaves the recent
|
||||
// one in history.
|
||||
// Empty summaries are retried without shrinking the history; the recovered
|
||||
// summary replaces the whole history with the real user messages plus the
|
||||
// prefixed summary.
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
{ role: 'user', text: `${COMPACTION_SUMMARY_PREFIX}\nRecovered compacted summary.` },
|
||||
]);
|
||||
expect(
|
||||
ctx.allEvents.filter((event) => event.event === 'compaction.completed'),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
args: expect.objectContaining({
|
||||
result: expect.objectContaining({ summary: 'Recovered compacted summary.' }),
|
||||
result: expect.objectContaining({
|
||||
summary: expect.stringContaining('Recovered compacted summary.'),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
vi.useRealTimers();
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
|
|
@ -692,13 +696,14 @@ describe('FullCompaction', () => {
|
|||
await completed;
|
||||
|
||||
expect(inputs).toHaveLength(2);
|
||||
// The retry compacts a strictly smaller prefix than the first attempt.
|
||||
// The retry sends a strictly smaller input than the first attempt.
|
||||
expect(inputs[1]!.length).toBeLessThan(inputs[0]!.length);
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
{ role: 'user', text: `${COMPACTION_SUMMARY_PREFIX}\nRecovered compacted summary.` },
|
||||
]);
|
||||
vi.useRealTimers();
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
|
|
@ -771,8 +776,10 @@ describe('FullCompaction', () => {
|
|||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
await failed;
|
||||
|
||||
// MAX_COMPACTION_RETRY_ATTEMPTS attempts, with prefix reduction between them.
|
||||
expect(inputs).toHaveLength(5);
|
||||
// Each empty/think-only response drops the oldest item and resets the retry
|
||||
// counter; once only one item remains, MAX_COMPACTION_RETRY_ATTEMPTS more
|
||||
// retries run before failing. 3 drops + 5 retries = 8 generate calls.
|
||||
expect(inputs).toHaveLength(8);
|
||||
expect(inputs[1]!.length).toBeLessThan(inputs[0]!.length);
|
||||
expect(records).toContainEqual({
|
||||
event: 'compaction_failed',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue