From 794db55538e01b4bf0c008c493de5d8b8bf67c5d Mon Sep 17 00:00:00 2001 From: qer Date: Sat, 27 Jun 2026 23:24:57 +0800 Subject: [PATCH] fix(agent-core): cap compaction output at 128k by default (#1156) --- .changeset/compaction-default-output-cap.md | 5 +++ .../agent-core/src/agent/compaction/full.ts | 25 ++++++++++++-- .../test/agent/compaction/full.test.ts | 33 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .changeset/compaction-default-output-cap.md diff --git a/.changeset/compaction-default-output-cap.md b/.changeset/compaction-default-output-cap.md new file mode 100644 index 000000000..f89cb0ed1 --- /dev/null +++ b/.changeset/compaction-default-output-cap.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Cap compaction output at 128k tokens by default to avoid provider max_tokens errors. diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts index e5f134df0..31ff5c4e4 100644 --- a/packages/agent-core/src/agent/compaction/full.ts +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -39,6 +39,15 @@ import { export const MAX_COMPACTION_RETRY_ATTEMPTS = 5; +/** + * Default hard cap on compaction output tokens when `maxOutputSize` is not + * configured on the model alias. Without this, compaction falls back to the + * full context window size, which exceeds the `max_tokens` ceiling enforced + * by many OpenAI-compatible providers. 128k matches the chat-completions + * ceiling applied by the OpenAI Legacy provider. + */ +const DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS = 128 * 1024; + class CompactionTruncatedError extends Error { constructor() { super('Compaction response was truncated before producing a complete summary.'); @@ -261,13 +270,25 @@ export class FullCompaction { await this.triggerPreCompactHook(data, tokensBefore, signal); const model = this.agent.config.model; + const capability = this.agent.config.modelCapabilities; + const maxContextTokens = capability.max_context_tokens; + // When the model's context window is known and the user has not set + // `maxOutputSize`, cap compaction output to a safe default so a large + // context window does not push `max_tokens` past the provider's ceiling. + // When the window is unknown (maxContextTokens === 0), leave + // `maxOutputSize` unset so `resolveCompletionBudget` falls back to the + // conservative unknown-context fallback. + const defaultCompactionCap = + maxContextTokens > 0 + ? Math.min(maxContextTokens, DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS) + : undefined; const provider = applyCompletionBudget({ provider: this.agent.config.provider, budget: resolveCompletionBudget({ - maxOutputSize: this.agent.config.maxOutputSize, + maxOutputSize: this.agent.config.maxOutputSize ?? defaultCompactionCap, reservedContextSize: this.agent.kimiConfig?.loopControl?.reservedContextSize, }), - capability: this.agent.config.modelCapabilities, + capability, }); const delays = retryBackoffDelays(MAX_COMPACTION_RETRY_ATTEMPTS); diff --git a/packages/agent-core/test/agent/compaction/full.test.ts b/packages/agent-core/test/agent/compaction/full.test.ts index 4e392908a..a1a630250 100644 --- a/packages/agent-core/test/agent/compaction/full.test.ts +++ b/packages/agent-core/test/agent/compaction/full.test.ts @@ -1811,6 +1811,39 @@ describe('FullCompaction', () => { expect(compactionMaxCompletionTokens).toEqual([384000]); }); + it('uses default 128k hardCap when maxOutputSize is not configured', async () => { + let callCount = 0; + const compactionMaxCompletionTokens: unknown[] = []; + const generate: GenerateFn = async (provider, _system, _tools, _history, callbacks) => { + callCount += 1; + if (callCount === 1) { + throw new APIContextOverflowError(400, 'Context length exceeded', 'req-default-cap'); + } + if (callCount === 2) { + compactionMaxCompletionTokens.push(providerMaxCompletionTokens(provider)); + return textResult('Default cap compacted summary.'); + } + await callbacks?.onMessagePart?.({ + type: 'text', + text: 'Recovered with default cap.', + }); + return textResult('Recovered with default cap.'); + }; + const ctx = testAgent({ generate }); + ctx.configure({ + provider: CATALOGUED_PROVIDER, + modelCapabilities: CATALOGUED_MODEL_CAPABILITIES, + }); + ctx.appendExchange(1, 'old user one', 'old assistant one', 20); + ctx.newEvents(); + + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Retry with default cap' }] }); + await ctx.untilTurnEnd(); + + expect(callCount).toBe(3); + expect(compactionMaxCompletionTokens).toEqual([128 * 1024]); + }); + it('ignores filtered assistant placeholders when checking the retained overflow suffix', async () => { let callCount = 0; const generate: GenerateFn = async (_provider, _system, _tools, _history, callbacks) => {