Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 4 additions & 37 deletions src/extension/intents/node/agentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<number | undefined>(ConfigKey.Advanced.SummarizeAgentConversationHistoryThreshold);
if (typeof summarizeThresholdOverride === 'number' && summarizeThresholdOverride < 100 && summarizeThresholdOverride > 0) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
});
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions src/extension/intents/node/askAgentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<vscode.LanguageModelToolInformation[]> {
Expand Down
4 changes: 1 addition & 3 deletions src/extension/intents/node/editCodeIntent2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<vscode.LanguageModelToolInformation[]> {
Expand Down
4 changes: 1 addition & 3 deletions src/extension/intents/node/notebookEditorIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 13 additions & 2 deletions src/extension/prompts/node/agent/summarizedConversationHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class ConversationHistorySummarizationPrompt extends PromptElement<Conver
</SystemMessage>
{history}
{this.props.workingNotebook && <WorkingNotebookSummary priority={this.props.priority - 2} notebook={this.props.workingNotebook} />}
<UserMessage>
<UserMessage priority={this.props.priority}>
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.<br />
{isOpus && <>
<br />
Expand Down Expand Up @@ -664,7 +664,18 @@ class ConversationHistorySummarizer {

private async getSummary(mode: SummaryMode, propsInfo: ISummarizedConversationHistoryInfo): Promise<SummarizationResult> {
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;
Expand Down
47 changes: 47 additions & 0 deletions src/extension/prompts/node/agent/test/summarization.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading