diff --git a/src/extension/intents/node/agentIntent.ts b/src/extension/intents/node/agentIntent.ts index fee2939405..80bc3dd999 100644 --- a/src/extension/intents/node/agentIntent.ts +++ b/src/extension/intents/node/agentIntent.ts @@ -19,7 +19,6 @@ import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogS import { CUSTOM_TOOL_SEARCH_NAME, isAnthropicCustomToolSearchEnabled, isAnthropicToolSearchEnabled } from '../../../platform/networking/common/anthropic'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { modelsWithoutResponsesContextManagement } from '../../../platform/networking/common/openai'; -import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics'; import { IOTelService } from '../../../platform/otel/common/otelService'; @@ -379,7 +378,6 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I @IExperimentationService private readonly expService: IExperimentationService, @IAutomodeService private readonly automodeService: IAutomodeService, @IOTelService override readonly otelService: IOTelService, - @IToolDeferralService private readonly toolDeferralService: IToolDeferralService, ) { super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, otelService); } @@ -405,15 +403,8 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I } const tools = promptContext.tools?.availableTools; - // When Anthropic tool search is enabled, deferred tools are sent with - // defer_loading: true and don't count against the context window until - // the model loads them via tool_search. Only count non-deferred tools - // so the budget isn't artificially reduced. const toolSearchEnabled = isAnthropicToolSearchEnabled(this.endpoint, this.configurationService); - const effectiveTools = tools && toolSearchEnabled - ? tools.filter(t => this.toolDeferralService.isNonDeferredTool(t.name)) - : tools; - const toolTokens = effectiveTools?.length ? await this.endpoint.acquireTokenizer().countToolTokens(effectiveTools) : 0; + const toolTokens = tools?.length ? await this.endpoint.acquireTokenizer().countToolTokens(tools) : 0; const summarizeThresholdOverride = this.configurationService.getConfig(ConfigKey.Advanced.SummarizeAgentConversationHistoryThreshold); if (typeof summarizeThresholdOverride === 'number' && summarizeThresholdOverride < 100 && summarizeThresholdOverride > 0) { @@ -441,7 +432,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I const safeBudget = useTruncation ? Number.MAX_SAFE_INTEGER : messageBudget; const endpoint = toolTokens > 0 ? this.endpoint.cloneWithTokenOverride(safeBudget) : this.endpoint; - this.logService.debug(`AgentIntent: rendering with budget=${safeBudget} (baseBudget: ${baseBudget}, toolTokens: ${toolTokens}${toolSearchEnabled ? `, totalTools: ${tools?.length ?? 0}, nonDeferredTools: ${effectiveTools?.length ?? 0}` : ''}), summarizationEnabled=${summarizationEnabled}`); + this.logService.debug(`AgentIntent: rendering with budget=${safeBudget} (baseBudget: ${baseBudget}, toolTokens: ${toolTokens}, totalTools: ${tools?.length ?? 0}, toolSearchEnabled: ${toolSearchEnabled}), summarizationEnabled=${summarizationEnabled}`); let result: RenderPromptResult; const props: AgentPromptProps = { endpoint, @@ -595,7 +586,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, { ...renderProps, endpoint: this.endpoint, - promptContext: this._buildSummarizationPromptContext(renderProps.promptContext), + promptContext: renderProps.promptContext, triggerSummarize: true, }); return await renderer.render(progress, token); @@ -868,7 +859,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I const bgRenderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, { ...snapshotProps, endpoint: this.endpoint, - promptContext: this._buildSummarizationPromptContext(snapshotProps.promptContext), + promptContext: snapshotProps.promptContext, triggerSummarize: true, summarizationSource: 'background', }); @@ -978,30 +969,6 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I )); } - /** - * Build a promptContext for summarization that filters availableTools to - * non-deferred tools when Anthropic tool search is enabled. Deferred tool - * schemas are unnecessary in the summarization prompt (which uses - * tool_choice: 'none') and can push the prompt over the token budget. - */ - private _buildSummarizationPromptContext(promptContext: IBuildPromptContext): IBuildPromptContext { - if (!promptContext.tools?.availableTools) { - return promptContext; - } - const toolSearchEnabled = isAnthropicToolSearchEnabled(this.endpoint, this.configurationService); - if (!toolSearchEnabled) { - return promptContext; - } - const nonDeferredTools = promptContext.tools.availableTools.filter(t => this.toolDeferralService.isNonDeferredTool(t.name)); - return { - ...promptContext, - tools: { - ...promptContext.tools, - availableTools: nonDeferredTools, - }, - }; - } - /** * Record a background compaction failure on the current turn's metadata, * matching how foreground compaction records its failures. diff --git a/src/extension/intents/node/askAgentIntent.ts b/src/extension/intents/node/askAgentIntent.ts index d1cbbd9a50..3be37f4426 100644 --- a/src/extension/intents/node/askAgentIntent.ts +++ b/src/extension/intents/node/askAgentIntent.ts @@ -12,7 +12,6 @@ import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; -import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { IOTelService } from '../../../platform/otel/common/otelService'; import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; @@ -129,9 +128,8 @@ export class AskAgentIntentInvocation extends AgentIntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, - @IToolDeferralService toolDeferralService: IToolDeferralService, ) { - super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, toolDeferralService); + super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); } public override async getAvailableTools(): Promise { diff --git a/src/extension/intents/node/editCodeIntent2.ts b/src/extension/intents/node/editCodeIntent2.ts index 4eca293b9a..afc6281042 100644 --- a/src/extension/intents/node/editCodeIntent2.ts +++ b/src/extension/intents/node/editCodeIntent2.ts @@ -13,7 +13,6 @@ import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; -import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService'; import { requestHasNotebookRefs } from '../../../platform/notebook/common/helpers'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { IOTelService } from '../../../platform/otel/common/otelService'; @@ -90,9 +89,8 @@ export class EditCode2IntentInvocation extends AgentIntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, - @IToolDeferralService toolDeferralService: IToolDeferralService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, toolDeferralService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); } public override async getAvailableTools(): Promise { diff --git a/src/extension/intents/node/notebookEditorIntent.ts b/src/extension/intents/node/notebookEditorIntent.ts index ee33040933..1b0cfdd866 100644 --- a/src/extension/intents/node/notebookEditorIntent.ts +++ b/src/extension/intents/node/notebookEditorIntent.ts @@ -12,7 +12,6 @@ import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; -import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService'; import { IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent'; import { getCellId } from '../../../platform/notebook/common/helpers'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; @@ -108,9 +107,8 @@ export class NotebookEditorIntentInvocation extends EditCode2IntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, - @IToolDeferralService toolDeferralService: IToolDeferralService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, toolDeferralService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); } protected override prompt = NotebookInlinePrompt; diff --git a/src/extension/prompts/node/agent/summarizedConversationHistory.tsx b/src/extension/prompts/node/agent/summarizedConversationHistory.tsx index 5344f22872..864d719e27 100644 --- a/src/extension/prompts/node/agent/summarizedConversationHistory.tsx +++ b/src/extension/prompts/node/agent/summarizedConversationHistory.tsx @@ -170,7 +170,7 @@ export class ConversationHistorySummarizationPrompt extends PromptElement {history} {this.props.workingNotebook && } - + Summarize the conversation history so far, paying special attention to the most recent agent commands and tool results that triggered this summarization. Structure your summary using the enhanced format provided in the system message.
{isOpus && <>
@@ -664,7 +664,18 @@ class ConversationHistorySummarizer { private async getSummary(mode: SummaryMode, propsInfo: ISummarizedConversationHistoryInfo): Promise { const stopwatch = new StopWatch(false); - const endpoint = this.props.endpoint; + + // In Full mode, tools are sent alongside the summarization prompt with + // tool_choice: 'none'. Reserve budget for them so the rendered messages + // plus tools don't exceed the model's context window. + const tools = this.props.tools; + const toolTokens = mode === SummaryMode.Full && tools?.length + ? await this.props.endpoint.acquireTokenizer().countToolTokens(tools) + : 0; + const endpoint = toolTokens > 0 + ? this.props.endpoint.cloneWithTokenOverride( + Math.max(1, Math.floor((this.props.endpoint.modelMaxPromptTokens - toolTokens) * 0.9))) + : this.props.endpoint; let summarizationPrompt: ChatMessage[]; const associatedRequestId = this.props.promptContext.conversation?.getLatestTurn().id; diff --git a/src/extension/prompts/node/agent/test/summarization.spec.tsx b/src/extension/prompts/node/agent/test/summarization.spec.tsx index 86a75410d1..048bc20592 100644 --- a/src/extension/prompts/node/agent/test/summarization.spec.tsx +++ b/src/extension/prompts/node/agent/test/summarization.spec.tsx @@ -440,6 +440,53 @@ suite('Agent Summarization', () => { } }); + test('simple mode summarization with small token budget renders zero messages (repro for No messages provided)', async () => { + // Repro for: "Prompt failed validation with the reason: No messages provided" + // + // Root cause: when modelMaxPromptTokens is small enough that the summarization + // prompt content exceeds the budget, prompt-tsx prunes all child elements. + // After pruning, toChatMessages() silently skips messages whose content is + // empty (isEmpty check), producing an empty messages array — without throwing + // BudgetExceededError. The downstream makeChatRequest2 then hits the + // isValidChatPayload check: "No messages provided". + const instaService = accessor.get(IInstantiationService); + const endpoint = instaService.createInstance(MockEndpoint, 'claude-sonnet'); + endpoint.modelMaxPromptTokens = 5; // So small that even a single short message cannot fit + + const toolCallRounds = [ + new ToolCallRound('ok', [createEditFileToolCall(1)]), + new ToolCallRound('ok 2', [createEditFileToolCall(2)]), + ]; + + const turn = new Turn('turnId', { type: 'user', message: 'hello' }); + const testConversation = new Conversation('sessionId', [turn]); + + const promptContext: IBuildPromptContext = { + chatVariables: new ChatVariablesCollection([]), + history: [], + query: 'edit this file', + toolCallRounds, + toolCallResults: createEditFileToolResult(1, 2), + tools, + conversation: testConversation, + }; + + const baseProps = { + priority: 1, + endpoint, + location: ChatLocation.Panel, + promptContext, + maxToolResultLength: Infinity, + }; + + const propsInfo = instaService.createInstance(SummarizedConversationHistoryPropsBuilder).getProps(baseProps); + const renderer = PromptRenderer.create(instaService, endpoint, ConversationHistorySummarizationPrompt, { ...propsInfo.props, simpleMode: true }); + const result = await renderer.render(); + + // prompt-tsx prunes all content and silently drops empty messages → 0 messages + expect(result.messages.length).toBe(0); + }); + test('failure metadata on turn prevents repeated foreground summarization attempts', async () => { // This test verifies the contract that agentIntent.ts relies on: // after a foreground summarization failure, setting SummarizedConversationHistoryMetadata