Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit 59434e4

Browse files
authored
Fix background summarization fallback gaps and improve summarization budget (#4981)
* Fix background summarization fallback gaps and improve summarization budget - Record failure metadata on turn for all background compaction noResult paths, matching how foreground compaction tracks failures - Add foreground summarization fallback when post-render background compaction produces no usable result at ≥95% context usage - Use unreduced endpoint for both foreground and background summarization since they are separate LLM calls that don't share token space - Filter deferred tool schemas from summarization prompt when Anthropic tool search is enabled to prevent budget_exceeded in Full mode - Add Full→Simple fallback log and SimulateSummarizationError debug setting - Deduplicate explanatory comments at summarization call sites - Add tests for background summarizer noResult state machine paths and background failure metadata recording on turns * Address Copilot review comments - Remove unused endpoint parameter from _startBackgroundSummarization - Remove duplicate backgroundSummarizer test (overlaps with existing) - Clarify summarization.spec.tsx tests as contract tests
1 parent 343b8bb commit 59434e4

2 files changed

Lines changed: 105 additions & 6 deletions

File tree

src/extension/intents/node/agentIntent.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,10 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
510510
this._persistSummaryOnTurn(bgResult, promptContext, this._lastRenderTokenCount);
511511
this._sendBackgroundCompactionTelemetry('preRender', 'applied', contextRatio, promptContext);
512512
summaryAppliedThisIteration = true;
513+
} else {
514+
this.logService.warn(`[Agent] background compaction state was Completed but consumeAndReset returned no result`);
515+
this._sendBackgroundCompactionTelemetry('preRender', 'noResult', contextRatio, promptContext);
516+
this._recordBackgroundCompactionFailure(promptContext, 'preRender');
513517
}
514518
}
515519

@@ -531,8 +535,14 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
531535
this._sendBackgroundCompactionTelemetry('preRenderBlocked', 'applied', contextRatio, promptContext);
532536
summaryAppliedThisIteration = true;
533537
} else {
534-
this.logService.debug(`[Agent] background compaction finished but produced no usable result`);
538+
this.logService.debug(`[Agent] background compaction finished but produced no usable result — will attempt foreground summarization if budget exceeded`);
535539
this._sendBackgroundCompactionTelemetry('preRenderBlocked', 'noResult', contextRatio, promptContext);
540+
this._recordBackgroundCompactionFailure(promptContext, 'preRenderBlocked');
541+
// Don't attempt a foreground fallback here — the main render below
542+
// will either succeed (context estimate was pessimistic) or throw
543+
// BudgetExceededError, which the catch block handles with foreground
544+
// summarization. Short-circuiting here would skip the main render
545+
// unnecessarily when it might still fit.
536546
}
537547
}
538548

@@ -582,8 +592,10 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
582592

583593
this.logService.debug(`[Agent] ${reason}, triggering summarization`);
584594
try {
585-
const renderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, {
595+
const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, {
586596
...renderProps,
597+
endpoint: this.endpoint,
598+
promptContext: this._buildSummarizationPromptContext(renderProps.promptContext),
587599
triggerSummarize: true,
588600
});
589601
return await renderer.render(progress, token);
@@ -692,6 +704,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
692704
} else {
693705
this.logService.debug(`[Agent] background compaction produced no usable result after budget exceeded — falling back to synchronous summarization`);
694706
this._sendBackgroundCompactionTelemetry(budgetExceededTrigger, 'noResult', contextRatio, promptContext);
707+
this._recordBackgroundCompactionFailure(promptContext, budgetExceededTrigger);
695708
// Background compaction failed — fall back to synchronous summarization
696709
result = await renderWithSummarization(`budget exceeded(${e.message}), background compaction failed`);
697710
}
@@ -758,12 +771,19 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
758771
result = await reRenderer.render(progress, token);
759772
this._lastRenderTokenCount = result.tokenCount;
760773
} else {
761-
this.logService.debug(`[Agent] post-render background compaction finished but produced no usable result`);
774+
this.logService.debug(`[Agent] post-render background compaction finished but produced no usable result — falling back to foreground summarization`);
762775
this._sendBackgroundCompactionTelemetry('postRenderBlocked', 'noResult', postRenderRatio, promptContext);
776+
this._recordBackgroundCompactionFailure(promptContext, 'postRenderBlocked');
777+
try {
778+
result = await renderWithSummarization('post-render background compaction noResult fallback');
779+
this._lastRenderTokenCount = result.tokenCount;
780+
} catch (e) {
781+
this.logService.error(e, `[Agent] post-render foreground summarization fallback also failed — using original render result`);
782+
}
763783
}
764784
} else if (postRenderRatio >= 0.80 && (backgroundSummarizer.state === BackgroundSummarizationState.Idle || backgroundSummarizer.state === BackgroundSummarizationState.Failed)) {
765785
// At ≥ 80% with no running compaction (or a previous failure) — kick off background work.
766-
this._startBackgroundSummarization(backgroundSummarizer, props, endpoint, token, postRenderRatio);
786+
this._startBackgroundSummarization(backgroundSummarizer, props, token, postRenderRatio);
767787
}
768788
}
769789

@@ -831,7 +851,6 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
831851
private _startBackgroundSummarization(
832852
backgroundSummarizer: BackgroundSummarizer,
833853
props: AgentPromptProps,
834-
endpoint: IChatEndpoint,
835854
token: vscode.CancellationToken,
836855
contextRatio: number,
837856
): void {
@@ -846,8 +865,10 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
846865
toolCallResults: props.promptContext.toolCallResults ? { ...props.promptContext.toolCallResults } : undefined,
847866
}
848867
};
849-
const bgRenderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, {
868+
const bgRenderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, {
850869
...snapshotProps,
870+
endpoint: this.endpoint,
871+
promptContext: this._buildSummarizationPromptContext(snapshotProps.promptContext),
851872
triggerSummarize: true,
852873
summarizationSource: 'background',
853874
});
@@ -957,6 +978,48 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
957978
));
958979
}
959980

981+
/**
982+
* Build a promptContext for summarization that filters availableTools to
983+
* non-deferred tools when Anthropic tool search is enabled. Deferred tool
984+
* schemas are unnecessary in the summarization prompt (which uses
985+
* tool_choice: 'none') and can push the prompt over the token budget.
986+
*/
987+
private _buildSummarizationPromptContext(promptContext: IBuildPromptContext): IBuildPromptContext {
988+
if (!promptContext.tools?.availableTools) {
989+
return promptContext;
990+
}
991+
const toolSearchEnabled = isAnthropicToolSearchEnabled(this.endpoint, this.configurationService);
992+
if (!toolSearchEnabled) {
993+
return promptContext;
994+
}
995+
const nonDeferredTools = promptContext.tools.availableTools.filter(t => this.toolDeferralService.isNonDeferredTool(t.name));
996+
return {
997+
...promptContext,
998+
tools: {
999+
...promptContext.tools,
1000+
availableTools: nonDeferredTools,
1001+
},
1002+
};
1003+
}
1004+
1005+
/**
1006+
* Record a background compaction failure on the current turn's metadata,
1007+
* matching how foreground compaction records its failures.
1008+
*/
1009+
private _recordBackgroundCompactionFailure(promptContext: IBuildPromptContext, trigger: string): void {
1010+
const turn = promptContext.conversation?.getLatestTurn();
1011+
turn?.setMetadata(new SummarizedConversationHistoryMetadata(
1012+
'', // no toolCallRoundId for failures
1013+
'', // no summary text for failures
1014+
{
1015+
model: this.endpoint.model,
1016+
source: 'background',
1017+
outcome: `noResult_${trigger}`,
1018+
contextLengthBefore: this._lastRenderTokenCount,
1019+
},
1020+
));
1021+
}
1022+
9601023
private _sendBackgroundCompactionTelemetry(
9611024
trigger: string,
9621025
outcome: string,

src/extension/prompts/node/agent/test/backgroundSummarizer.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,40 @@ describe('BackgroundSummarizer', () => {
216216
]);
217217
expect(summarizer.state).toBe(BackgroundSummarizationState.Completed);
218218
});
219+
220+
test('waitForCompletion resolves without error even when work fails', async () => {
221+
// agentIntent.ts calls waitForCompletion without try/catch in the
222+
// blocking paths — verify it swallows the error.
223+
const summarizer = new BackgroundSummarizer(100_000);
224+
summarizer.start(async _token => {
225+
throw new Error('network timeout');
226+
});
227+
// Should not throw
228+
await summarizer.waitForCompletion();
229+
expect(summarizer.state).toBe(BackgroundSummarizationState.Failed);
230+
});
231+
232+
test('cancel during waitForCompletion leaves state Idle with no result', async () => {
233+
// Tests the race where a caller is awaiting completion and cancellation happens
234+
const summarizer = new BackgroundSummarizer(100_000);
235+
let resolveFn: () => void;
236+
const gate = new Promise<void>(resolve => { resolveFn = resolve; });
237+
summarizer.start(async _token => {
238+
await gate;
239+
return { summary: 'test', toolCallRoundId: 'r1' };
240+
});
241+
// Start awaiting completion (captures the promise but doesn't resolve yet)
242+
const completionPromise = summarizer.waitForCompletion();
243+
// Cancel while waitForCompletion is pending
244+
summarizer.cancel();
245+
// Let the work resolve so the promise settles
246+
resolveFn!();
247+
await completionPromise;
248+
await new Promise<void>(resolve => setTimeout(resolve, 0));
249+
250+
// State should be Idle (cancel resets) and no result available
251+
const result = summarizer.consumeAndReset();
252+
expect(result).toBeUndefined();
253+
expect(summarizer.state).toBe(BackgroundSummarizationState.Idle);
254+
});
219255
});

0 commit comments

Comments
 (0)