Skip to content
126 changes: 44 additions & 82 deletions extensions/copilot/src/extension/intents/node/agentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ITestProvider } from '../../../platform/testing/common/testProvider';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';

import { findLast } from '../../../util/vs/base/common/arraysFind';
import { raceTimeout } from '../../../util/vs/base/common/async';
import { isCancellationError } from '../../../util/vs/base/common/errors';
import { Iterable } from '../../../util/vs/base/common/iterator';
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
Expand All @@ -44,13 +45,11 @@ import { Conversation, normalizeSummariesOnRounds, RenderedUserMessageMetadata,
import { IBuildPromptContext, InternalToolReference } from '../../prompt/common/intents';
import { getRequestedToolCallIterationLimit, IContinueOnErrorConfirmation } from '../../prompt/common/specialRequestTypes';
import { ChatTelemetryBuilder } from '../../prompt/node/chatParticipantTelemetry';
import { IntentInvocationMetadata } from '../../prompt/node/conversation';
import { IDefaultIntentRequestHandlerOptions } from '../../prompt/node/defaultIntentRequestHandler';
import { IDocumentContext } from '../../prompt/node/documentContext';
import { IBuildPromptResult, IIntent, IIntentInvocation } from '../../prompt/node/intents';
import { AgentPrompt, AgentPromptProps } from '../../prompts/node/agent/agentPrompt';
import { BackgroundSummarizationState, BackgroundSummarizationThresholds, BackgroundSummarizer, IBackgroundSummarizationResult, shouldKickOffBackgroundSummarization } from '../../prompts/node/agent/backgroundSummarizer';
import { BackgroundTodoDecision, BackgroundTodoProcessor, IBackgroundTodoExecutionContext } from '../../prompts/node/agent/backgroundTodoProcessor';
import { AgentPromptCustomizations, PromptRegistry } from '../../prompts/node/agent/promptRegistry';
import { extractSummary, SummarizationUserMessage, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder, appendTranscriptHintToSummary, computeSummarizationRoundCounts } from '../../prompts/node/agent/summarizedConversationHistory';
import { PromptRenderer, renderPromptElement } from '../../prompts/node/base/promptRenderer';
Expand All @@ -70,6 +69,7 @@ import { addCacheBreakpoints } from './cacheBreakpoints';
import { EditCodeIntent, EditCodeIntentInvocation, EditCodeIntentInvocationOptions, mergeMetadata, toNewChatReferences } from './editCodeIntent';
import { ToolCallingLoop } from './toolCallingLoop';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { BackgroundTodoAgentProcessor, getSessionResource } from '../../prompts/node/agent/backgroundTodoAgent/backgroundTodoAgentProcessor';

function isResponsesCompactionContextManagementEnabled(endpoint: IChatEndpoint, configurationService: IConfigurationService, experimentationService: IExperimentationService): boolean {
return endpoint.apiType === 'responses'
Expand Down Expand Up @@ -291,7 +291,7 @@ export class AgentIntent extends EditCodeIntent {
override readonly id = AgentIntent.ID;

private readonly _backgroundSummarizers = new Map<string, BackgroundSummarizer>();
private readonly _backgroundTodoProcessors = new Map<string, BackgroundTodoProcessor>();
private readonly _backgroundTodoProcessors = new Map<string, BackgroundTodoAgentProcessor>();

constructor(
@IInstantiationService instantiationService: IInstantiationService,
Expand All @@ -303,6 +303,8 @@ export class AgentIntent extends EditCodeIntent {
@IChatSessionService chatSessionService: IChatSessionService,
@IAutomodeService private readonly _automodeService: IAutomodeService,
@ILogService private readonly _logService: ILogService,
@IToolsService private readonly _toolsService: IToolsService,
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super(instantiationService, endpointProvider, configurationService, expService, codeMapperService, workspaceService, { intentInvocation: AgentIntentInvocation, processCodeblocks: false });
chatSessionService.onDidDisposeChatSession(sessionId => {
Expand Down Expand Up @@ -350,10 +352,21 @@ export class AgentIntent extends EditCodeIntent {
}
}

getOrCreateBackgroundTodoProcessor(sessionId: string): BackgroundTodoProcessor {
getOrCreateBackgroundTodoProcessor(promptContext: IBuildPromptContext): BackgroundTodoAgentProcessor | undefined {
const sessionId = promptContext.conversation?.sessionId;
if (sessionId === undefined) {
return undefined;
}
let processor = this._backgroundTodoProcessors.get(sessionId);
if (!processor) {
processor = new BackgroundTodoProcessor(this._logService);
processor = new BackgroundTodoAgentProcessor(
sessionId,
getSessionResource(promptContext),
this._toolsService,
this._telemetryService,
this.instantiationService,
this._logService
);
this._backgroundTodoProcessors.set(sessionId, processor);
}
return processor;
Expand Down Expand Up @@ -389,19 +402,17 @@ export class AgentIntent extends EditCodeIntent {
// Fire one final bg todo review pass once the agent loop has ended for
// this turn. The per-round passes never see the very last round, so any
// task that just completed otherwise stays stuck as 'in-progress'.
// Await completion so the tool invocation runs while the request is
// still active — the platform rejects tool calls for completed requests.
// Do NOT pass the request `token` as parentToken — it may be cancelled
// by the framework after the turn ends, which would immediately abort
// the background pass even on a normal completion.
const todoProcessor = this._backgroundTodoProcessors.get(conversation.sessionId);
if (todoProcessor !== undefined) {
const currentTurn = conversation.getLatestTurn();
const invocation = currentTurn.getMetadata(IntentInvocationMetadata)?.value;
const executionContext = invocation instanceof AgentIntentInvocation ? invocation.getBackgroundTodoExecutionContext() : undefined;
if (executionContext) {
todoProcessor.requestFinalReview(currentTurn.id, executionContext);
await todoProcessor.waitForCompletion();
// Await completion so this final pass runs before we return, while the
// request's tool invocation token is (hopefully) still valid.

if (request.subAgentInvocationId === undefined && request.subAgentName === undefined) {
const todoProcessor = this._backgroundTodoProcessors.get(conversation.sessionId);
if (todoProcessor) {
await raceTimeout(
todoProcessor.endTurn(conversation.getLatestTurn().id, request.toolInvocationToken),
5000,
() => todoProcessor.cancel()
);
Comment thread
vritant24 marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -547,8 +558,6 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
/** Cached model capabilities from the most recent main agent render, reused by the background summarizer. */
private _lastModelCapabilities: { enableThinking: boolean; reasoningEffort: string | undefined; enableToolSearch: boolean; enableContextEditing: boolean } | undefined;

private _backgroundTodoExecutionContext: IBackgroundTodoExecutionContext | undefined;

/**
* RNG used to jitter the background-summarization trigger threshold around 0.80.
* Tests may overwrite this directly (e.g. `(invocation as any)._thresholdRng = () => 0.5`).
Expand Down Expand Up @@ -939,8 +948,8 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
}
}

// ── Background todo processing ──────────────────────────────────
this._maybeStartBackgroundTodoPass(endpoint, promptContext, token);
// Background todo processing
this._maybeStartBackgroundTodoAgentPass(endpoint, promptContext, token);

const lastMessage = result.messages.at(-1);
if (lastMessage?.role === Raw.ChatRole.User) {
Expand Down Expand Up @@ -1341,75 +1350,28 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
}

// ── Background todo processing ──────────────────────────────────

/**
* Returns the `BackgroundTodoProcessor` for this session, or `undefined`
* if the intent is not an `AgentIntent`.
*/
private _getOrCreateBackgroundTodoProcessor(sessionId: string | undefined): BackgroundTodoProcessor | undefined {
if (!sessionId || !(this.intent instanceof AgentIntent)) {
private _getOrCreateBackgroundTodoAgentProcessor(promptContext: IBuildPromptContext) {
if (!(this.intent instanceof AgentIntent)) {
return undefined;
}
return this.intent.getOrCreateBackgroundTodoProcessor(sessionId);
}

getBackgroundTodoExecutionContext(): IBackgroundTodoExecutionContext | undefined {
return this._backgroundTodoExecutionContext;
return this.intent.getOrCreateBackgroundTodoProcessor(promptContext);
}

/**
* Kick off a background todo pass if the policy says to run.
*/
private _maybeStartBackgroundTodoPass(
endpoint: IChatEndpoint,
promptContext: IBuildPromptContext,
token: vscode.CancellationToken,
): void {
// Subagent requests must not drive background todo passes. The main
// agent's next render will see all accumulated rounds from subagents
// via the delta tracker and trigger a single consolidated pass then.
if (this.request.subAgentInvocationId) {
return;
}

const sessionId = promptContext.conversation?.sessionId;
const processor = this._getOrCreateBackgroundTodoProcessor(sessionId);
if (!processor) {
return;
}

const turnId = promptContext.conversation?.getLatestTurn()?.id;
const executionContext: IBackgroundTodoExecutionContext = {
instantiationService: this.instantiationService,
logService: this.logService,
toolsService: this.toolsService,
telemetryService: this.telemetryService,
promptContext,
};

const { decision, reason, delta } = processor.shouldRun({
backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(endpoint, this.configurationService, this.expService, this.authenticationService, this.request),
todoToolExplicitlyEnabled: isTodoToolExplicitlyEnabled(this.request),
isAgentPrompt: this.prompt === AgentPrompt,
promptContext,
turnId,
});

this.logService.debug(`[BackgroundTodo] policy decision: ${decision} (${reason})`);

if (decision === BackgroundTodoDecision.Wait && reason === 'processorInProgress' && delta) {
// Coalesce into the queue so the latest context is not lost.
this._backgroundTodoExecutionContext = executionContext;
processor.requestRegularPass(delta, executionContext, token, turnId);
private _maybeStartBackgroundTodoAgentPass(endpoint: IChatEndpoint, promptContext: IBuildPromptContext, token: vscode.CancellationToken) {
if (
!isBackgroundTodoAgentEnabled(endpoint, this.configurationService, this.expService, this.authenticationService, this.request) ||
isTodoToolExplicitlyEnabled(this.request) ||
this.request.subAgentInvocationId !== undefined ||
this.request.subAgentName !== undefined
) {
return;
}

if (decision !== BackgroundTodoDecision.Run || !delta) {
const processor = this._getOrCreateBackgroundTodoAgentProcessor(promptContext);
if (processor === undefined) {
return;
}

this._backgroundTodoExecutionContext = executionContext;
processor.requestRegularPass(delta, executionContext, token, turnId);
processor.trackTurnRound(promptContext, token);
}

override processResponse = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ChatParticipantToolToken } from 'vscode';
import { createServiceIdentifier } from '../../../util/common/services';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { LanguageModelTextPart } from '../../../vscodeTypes';
Expand All @@ -12,6 +13,7 @@ import { IToolsService } from '../../tools/common/toolsService';
export const ITodoListContextProvider = createServiceIdentifier<ITodoListContextProvider>('ITodoListContextProvider');
export interface ITodoListContextProvider {
getCurrentTodoContext(sessionResource: string): Promise<string | undefined>;
clearCurrentTodoContext(toolInvocationToken: ChatParticipantToolToken): Promise<void>;
}

export class TodoListContextProvider implements ITodoListContextProvider {
Expand Down Expand Up @@ -47,4 +49,20 @@ export class TodoListContextProvider implements ITodoListContextProvider {
return undefined;
}
}

async clearCurrentTodoContext(toolInvocationToken: ChatParticipantToolToken): Promise<void> {
try {
await this.toolsService.invokeTool(
ToolName.CoreManageTodoList,
{
input: { operation: 'write', todoList: [] },
toolInvocationToken,
},
CancellationToken.None
);
} catch (error) {
// Ignore failures when clearing the todo context
}
}

}
Loading
Loading