diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index bfb9ab13cd..6d3e923f23 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -648,6 +648,35 @@ describe('summarizationEnabled resolution', () => { expect(config.provider).toBe('openAI'); expect(config.model).toBe('gpt-4o'); }); + + it('false when the effective context budget is below the viable minimum', async () => { + /** + * A tiny user-set maxContextTokens re-triggers summarization on every + * graph step until the recursion limit aborts the run; the guard falls + * back to plain pruning instead. + */ + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 10 })], + summarizationConfig: { + enabled: true, + provider: 'anthropic', + model: 'claude-3-haiku', + }, + }); + expect(agents[0].summarizationEnabled).toBe(false); + }); + + it('true at exactly the 1024-token viable minimum', async () => { + const agents = await callAndCapture({ + agents: [makeAgent({ maxContextTokens: 1024 })], + summarizationConfig: { + enabled: true, + provider: 'anthropic', + model: 'claude-3-haiku', + }, + }); + expect(agents[0].summarizationEnabled).toBe(true); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 77cff7a4e7..648d71d572 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -941,6 +941,18 @@ function shapeSummarizationConfig( }; } +/** + * Below this context budget a summarization cycle cannot make progress: the + * summary allocation rounds down to a handful of tokens, the rewritten history + * still overflows, and the graph re-triggers summarization on every step until + * the recursion limit aborts the run — dozens of wasted LLM calls surfaced to + * the user as an opaque LangGraph error. Falling back to plain pruning instead + * either fits the request or fails fast with the actionable `empty_messages` + * token-budget breakdown. Matches the floor `initializeAgent` applies when the + * user supplies no override. + */ +const MIN_SUMMARIZATION_CONTEXT_TOKENS = 1024; + /** * Applies `reserveRatio` against the pre-ratio base context budget, falling * back to the pre-computed `maxContextTokens` from initializeAgent. @@ -1879,6 +1891,20 @@ export async function createRun({ agent.maxContextTokens, ); + const summarizationViable = + effectiveMaxContextTokens == null || + effectiveMaxContextTokens >= MIN_SUMMARIZATION_CONTEXT_TOKENS; + if (summarization.enabled && !summarizationViable) { + logger.warn( + '[createRun] Summarization disabled for this run: context budget below viable minimum', + { + agentId: agent.id, + effectiveMaxContextTokens, + minimum: MIN_SUMMARIZATION_CONTEXT_TOKENS, + }, + ); + } + const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint, agent.reasoningKey); const agentInput: AgentInputs = { provider, @@ -1896,7 +1922,7 @@ export async function createRun({ useLegacyContent: agent.useLegacyContent ?? false, discoveredTools: !isSubagent && discoveredTools.size > 0 ? Array.from(discoveredTools) : undefined, - summarizationEnabled: summarization.enabled, + summarizationEnabled: summarization.enabled && summarizationViable, summarizationConfig: summarization.config, ...(!isSubagent && compactionSemanticIndex != null ? { compactionSemanticIndex } : {}), initialSummary: isSubagent ? undefined : initialSummary,