Path: blob/main/extensions/copilot/src/extension/prompts/node/agent/backgroundSummarizer.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';67/**8* State machine for background conversation summarization.9*10* Lifecycle:11* Idle → InProgress → Completed / Failed12* ↓ ↓13* (consumeAndReset → Idle)14* Failed → InProgress (retry)15*/1617export const enum BackgroundSummarizationState {18/** No summarization running. */19Idle = 'Idle',20/** An LLM summarization request is in flight. */21InProgress = 'InProgress',22/** Summarization finished successfully — summary text is available. */23Completed = 'Completed',24/** Summarization failed. */25Failed = 'Failed',26}2728export interface IBackgroundSummarizationResult {29readonly summary: string;30readonly toolCallRoundId: string;31readonly promptTokens?: number;32readonly promptCacheTokens?: number;33readonly outputTokens?: number;34readonly durationMs?: number;35readonly model?: string;36readonly summarizationMode?: string;37readonly numRounds?: number;38readonly numRoundsSinceLastSummarization?: number;39}4041/**42* Thresholds used by {@link shouldKickOffBackgroundSummarization}. Exported so43* tests can reference the same numbers without repeating them.44*/45export const BackgroundSummarizationThresholds = {46/** Trigger ratio for the non-inline path (no prompt-cache benefit). */47base: 0.80,48/** Minimum of the jittered warm-cache range for the inline path. */49warmJitterMin: 0.78,50/** Width of the jittered warm-cache range; together with `warmJitterMin` yields [0.78, 0.82). */51warmJitterSpan: 0.04,52/**53* Cold-cache emergency ratio for the inline path. Above this we kick off54* even without a warmed cache to avoid forcing a foreground sync compaction55* on the next render. Tuned low enough that long-running sessions stay56* ahead of the budget without relying on foreground compaction.57*/58emergency: 0.90,59} as const;6061/**62* Decide whether to kick off post-render background compaction.63*64* For the inline-summarization path prompt-cache parity matters, so we:65* - require a completed tool call in this turn ("warm" cache) before66* firing at the normal, jittered ~0.80 threshold;67* - allow an emergency kick-off at >= 0.90 even with a cold cache to68* avoid forcing a foreground sync compaction on the next render.69*70* The jitter range straddles the historical 0.80 threshold (not "lower the71* bar") — the goal is to avoid always firing at the exact same boundary,72* not to kick off systematically earlier.73*74* The non-inline path forks its own prompt (no cache benefit) and keeps the75* simple >= 0.80 behavior. `rng` is only consumed on the warm-cache inline76* branch, which keeps deterministic tests straightforward.77*/78export function shouldKickOffBackgroundSummarization(79postRenderRatio: number,80useInlineSummarization: boolean,81cacheWarm: boolean,82rng: () => number,83): boolean {84const t = BackgroundSummarizationThresholds;85if (!useInlineSummarization) {86return postRenderRatio >= t.base;87}88if (!cacheWarm) {89return postRenderRatio >= t.emergency;90}91const jittered = t.warmJitterMin + rng() * t.warmJitterSpan;92return postRenderRatio >= jittered;93}9495/**96* Tracks a single background summarization pass for one chat session.97*98* The singleton `AgentIntent` owns one instance per session (keyed by99* `sessionId`). `AgentIntentInvocation.buildPrompt` queries the state100* on every tool-call iteration to decide whether to start, wait for, or101* apply a background summary.102*/103export class BackgroundSummarizer {104105private _state: BackgroundSummarizationState = BackgroundSummarizationState.Idle;106private _result: IBackgroundSummarizationResult | undefined;107private _error: unknown;108private _promise: Promise<void> | undefined;109private _cts: CancellationTokenSource | undefined;110111readonly modelMaxPromptTokens: number;112113get state(): BackgroundSummarizationState { return this._state; }114get error(): unknown { return this._error; }115116get token() { return this._cts?.token; }117118constructor(modelMaxPromptTokens: number) {119this.modelMaxPromptTokens = modelMaxPromptTokens;120}121122start(work: (token: CancellationToken) => Promise<IBackgroundSummarizationResult>, parentToken?: CancellationToken): void {123if (this._state !== BackgroundSummarizationState.Idle && this._state !== BackgroundSummarizationState.Failed) {124return; // already running or completed125}126127this._state = BackgroundSummarizationState.InProgress;128this._error = undefined;129this._cts = new CancellationTokenSource(parentToken);130const token = this._cts.token;131this._promise = work(token).then(132result => {133if (this._state !== BackgroundSummarizationState.InProgress) {134return; // cancelled while in flight135}136this._result = result;137this._state = BackgroundSummarizationState.Completed;138},139err => {140if (this._state !== BackgroundSummarizationState.InProgress) {141return; // cancelled while in flight142}143this._error = err;144this._state = BackgroundSummarizationState.Failed;145},146);147}148149async waitForCompletion(): Promise<void> {150if (this._promise) {151await this._promise;152}153}154155consumeAndReset(): IBackgroundSummarizationResult | undefined {156if (this._state === BackgroundSummarizationState.InProgress) {157return undefined;158}159const result = this._result;160this._state = BackgroundSummarizationState.Idle;161this._result = undefined;162this._error = undefined;163this._promise = undefined;164this._cts?.dispose();165this._cts = undefined;166return result;167}168169cancel(): void {170this._cts?.cancel();171this._cts?.dispose();172this._cts = undefined;173this._state = BackgroundSummarizationState.Idle;174this._result = undefined;175this._error = undefined;176this._promise = undefined;177}178}179180181