Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/retry-model-token-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kosong": patch
"@moonshot-ai/kimi-code": patch
---

Recover from provider model token limit errors during long conversations.
27 changes: 19 additions & 8 deletions packages/agent-core/src/agent/compaction/full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class FullCompaction {
startedAt: number;
telemetryTrigger: CompactionTelemetryTrigger;
promise: Promise<void>;
blockedByTurn: boolean;
} | null = null;
protected _compactedHistory: CompactedHistory[] = [];
protected readonly strategy: CompactionStrategy;
Expand Down Expand Up @@ -112,6 +113,7 @@ export class FullCompaction {
startedAt: Date.now(),
telemetryTrigger: compactionTelemetryTrigger(data.source, data.instruction),
promise: Promise.resolve(),
blockedByTurn: false,
};
this.compacting = active;
active.promise = this.compactionWorker(abortController.signal, data, compactedCount);
Expand Down Expand Up @@ -195,6 +197,7 @@ export class FullCompaction {
private async block(signal: AbortSignal): Promise<void> {
const active = this.compacting;
if (active) {
active.blockedByTurn = true;
signal.addEventListener('abort', () => {
if (this.compacting === active) {
this.cancel();
Expand Down Expand Up @@ -312,26 +315,34 @@ export class FullCompaction {
this.triggerPostCompactHook(data, result);
} catch (error) {
if (!isAbortError(error)) {
const active = this.compacting;
const blockedByTurn = active?.blockedByTurn === true;
this.agent.log.error('compaction failed', {
code: isKimiError(error) ? error.code : undefined,
error,
});
this.markCanceled();
const payload =
isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED
? toKimiErrorPayload(error)
: makeErrorPayload(ErrorCodes.COMPACTION_FAILED, String(error));
this.agent.emitEvent({
type: 'error',
...payload,
});
if (!blockedByTurn) {
const payload =
isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED
? toKimiErrorPayload(error)
: makeErrorPayload(ErrorCodes.COMPACTION_FAILED, String(error));
this.agent.emitEvent({
type: 'error',
...payload,
});
}
this.agent.telemetry.track('compaction_failed', {
trigger_type: compactionTelemetryTrigger(data.source, data.instruction),
before_tokens: tokensBefore,
duration_ms: Date.now() - startedAt,
retry_count: retryCount,
error_type: error instanceof Error ? error.name : 'Unknown',
});
if (blockedByTurn) {
if (isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED) throw error;
throw new KimiError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error });
}
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions packages/agent-core/test/agent/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,45 @@ describe('Agent compaction', () => {
await ctx.expectResumeMatches();
});

it('fails a blocked turn when auto compaction generation fails', async () => {
let attempts = 0;
const generate: GenerateFn = async () => {
attempts += 1;
throw new APIStatusError(400, 'Bad request');
};
const ctx = testAgent({ generate, compactionStrategy: alwaysCompactOnce });
ctx.configure();

await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Trigger failed auto compaction' }] });
const events = await ctx.untilTurnEnd();

expect(attempts).toBe(1);
expect(events).not.toContainEqual(expect.objectContaining({ event: 'error' }));
expect(events).toContainEqual(
expect.objectContaining({
event: 'turn.ended',
args: {
turnId: 0,
reason: 'failed',
error: expect.objectContaining({
code: 'compaction.failed',
message: 'APIStatusError: Bad request',
}),
},
}),
);
const errorEvents = ctx.newEvents();
expect(errorEvents).toHaveLength(1);
expect(errorEvents[0]).toMatchObject({
event: 'error',
args: expect.objectContaining({
code: 'compaction.failed',
message: 'APIStatusError: Bad request',
}),
});
await ctx.expectResumeMatches();
});

it('reports compaction retry_count when retryable generation failures are exhausted', async () => {
vi.useFakeTimers();
const records: TelemetryRecord[] = [];
Expand Down
1 change: 1 addition & 0 deletions packages/kosong/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const CONTEXT_OVERFLOW_MESSAGE_PATTERNS = [
/(?:too many tokens.*(?:prompt|input|context)|(?:prompt|input|context).*too many tokens)/,
/prompt is too long.*maximum/,
/input token count.*exceeds?.*maximum number of tokens/,
/request.*exceed(?:ed|s|ing)?.*model token limit/,
] as const;

export function normalizeAPIStatusError(
Expand Down
1 change: 1 addition & 0 deletions packages/kosong/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ describe('normalizeAPIStatusError', () => {
[422, 'Too many tokens in prompt'],
[400, 'prompt is too long: 210000 tokens exceeds the maximum'],
[400, 'input token count 131072 exceeds the maximum number of tokens allowed'],
[400, 'Invalid request: Your request exceeded model token limit: 262144 (requested: 274613)'],
])('normalizes %i "%s" to APIContextOverflowError', (statusCode, message) => {
const error = normalizeAPIStatusError(statusCode, message, 'req-context');
expect(error).toBeInstanceOf(APIContextOverflowError);
Expand Down
Loading