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

Commit a660818

Browse files
committed
Fix: strip tool_search messages from summarization to prevent tool_reference errors
The summarization call uses ChatLocation.Other but createMessagesRequestBody still converts custom tool_search results to tool_reference blocks because customToolSearchEnabled isn't gated by isAllowedConversationAgent. Without tool search enabled in the summarization request, Anthropic rejects tool_reference content blocks. Strip tool_search tool_use/tool_result pairs from messages before sending the summarization request.
1 parent 324d55e commit a660818

2 files changed

Lines changed: 149 additions & 1 deletion

File tree

src/extension/prompts/node/agent/summarizedConversationHistory.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IHistoricalTurn, ISessionTranscriptService } from '../../../../platform
1414
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
1515
import { isAnthropicFamily, isGeminiFamily } from '../../../../platform/endpoint/common/chatModelCapabilities';
1616
import { ILogService } from '../../../../platform/log/common/logService';
17+
import { CUSTOM_TOOL_SEARCH_NAME } from '../../../../platform/networking/common/anthropic';
1718
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
1819
import { APIUsage } from '../../../../platform/networking/common/openai';
1920
import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';
@@ -703,6 +704,16 @@ class ConversationHistorySummarizer {
703704
stripCacheBreakpoints(summarizationPrompt);
704705

705706
let messages = ToolCallingLoop.stripInternalToolCallIds(summarizationPrompt);
707+
708+
// Strip custom client-side tool search (tool_search) tool_use/tool_result
709+
// pairs. The summarization call uses ChatLocation.Other but
710+
// createMessagesRequestBody still converts tool_search results to
711+
// tool_reference blocks (customToolSearchEnabled isn't gated by location).
712+
// Without tool search enabled in the request, Anthropic rejects them.
713+
if (isAnthropicFamily(endpoint)) {
714+
messages = stripToolSearchMessages(messages);
715+
}
716+
706717
// Gemini strictly requires every function_call to have a matching function_response.
707718
// When prompt-tsx prunes tool result messages due to token budget, orphaned tool_calls
708719
// can remain, causing a 400 INVALID_ARGUMENT error. Strip them for Gemini models.
@@ -903,6 +914,44 @@ function stripCacheBreakpoints(messages: ChatMessage[]): void {
903914
});
904915
}
905916

917+
/**
918+
* Strip custom client-side tool search (tool_search) tool_use and tool_result
919+
* messages from the conversation. The summarization call uses ChatLocation.Other
920+
* but createMessagesRequestBody still converts tool_search results to
921+
* tool_reference blocks (customToolSearchEnabled isn't gated by location).
922+
* Without tool search enabled in the request, Anthropic rejects tool_reference
923+
* content blocks with: "Input tag 'tool_reference' found using 'type' does not
924+
* match any of the expected tags".
925+
*/
926+
export function stripToolSearchMessages(messages: ChatMessage[]): ChatMessage[] {
927+
const toolSearchIds = new Set<string>();
928+
for (const message of messages) {
929+
if (message.role === Raw.ChatRole.Assistant && message.toolCalls) {
930+
for (const tc of message.toolCalls) {
931+
if (tc.function.name === CUSTOM_TOOL_SEARCH_NAME) {
932+
toolSearchIds.add(tc.id);
933+
}
934+
}
935+
}
936+
}
937+
938+
if (toolSearchIds.size === 0) {
939+
return messages;
940+
}
941+
942+
return messages.map(message => {
943+
if (message.role === Raw.ChatRole.Assistant && message.toolCalls) {
944+
const filteredToolCalls = message.toolCalls.filter(tc => !toolSearchIds.has(tc.id));
945+
if (filteredToolCalls.length !== message.toolCalls.length) {
946+
return { ...message, toolCalls: filteredToolCalls.length > 0 ? filteredToolCalls : undefined };
947+
}
948+
} else if (message.role === Raw.ChatRole.Tool && message.toolCallId && toolSearchIds.has(message.toolCallId)) {
949+
return undefined;
950+
}
951+
return message;
952+
}).filter((m): m is ChatMessage => m !== undefined);
953+
}
954+
906955
export interface ISummarizedConversationHistoryInfo {
907956
readonly props: SummarizedAgentHistoryProps;
908957
readonly summarizedToolCallRoundId: string;

src/extension/prompts/node/agent/test/summarization.spec.tsx

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { ToolName } from '../../../../tools/common/toolNames';
3030
import { PromptRenderer } from '../../base/promptRenderer';
3131
import { AgentPrompt, AgentPromptProps } from '../agentPrompt';
3232
import { PromptRegistry } from '../promptRegistry';
33-
import { ConversationHistorySummarizationPrompt, extractInlineSummary, InlineSummarizationRequestedMetadata, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder } from '../summarizedConversationHistory';
33+
import { ConversationHistorySummarizationPrompt, extractInlineSummary, InlineSummarizationRequestedMetadata, stripToolSearchMessages, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder } from '../summarizedConversationHistory';
3434

3535
suite('Agent Summarization', () => {
3636
let accessor: ITestingServicesAccessor;
@@ -690,3 +690,102 @@ suite('Inline Summarization Prompt', () => {
690690
expect(inlineMeta).toBeDefined();
691691
});
692692
});
693+
694+
suite('stripToolSearchMessages', () => {
695+
function makeAssistantMessage(toolCalls: { id: string; name: string }[], text = 'response'): Raw.ChatMessage {
696+
return {
697+
role: Raw.ChatRole.Assistant,
698+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
699+
toolCalls: toolCalls.map(tc => ({
700+
type: 'function' as const,
701+
id: tc.id,
702+
function: { name: tc.name, arguments: '{}' },
703+
})),
704+
};
705+
}
706+
707+
function makeToolResult(toolCallId: string, text = 'result'): Raw.ChatMessage {
708+
return {
709+
role: Raw.ChatRole.Tool,
710+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
711+
toolCallId,
712+
};
713+
}
714+
715+
function makeUserMessage(text = 'hello'): Raw.ChatMessage {
716+
return {
717+
role: Raw.ChatRole.User,
718+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
719+
};
720+
}
721+
722+
test('returns messages unchanged when no tool_search calls present', () => {
723+
const messages = [
724+
makeUserMessage(),
725+
makeAssistantMessage([{ id: 'tc1', name: 'read_file' }]),
726+
makeToolResult('tc1'),
727+
];
728+
const result = stripToolSearchMessages(messages);
729+
expect(result).toBe(messages);
730+
});
731+
732+
test('strips custom tool_search tool_use and tool_result', () => {
733+
const messages = [
734+
makeUserMessage(),
735+
makeAssistantMessage([
736+
{ id: 'tc1', name: 'read_file' },
737+
{ id: 'tc2', name: 'tool_search' },
738+
]),
739+
makeToolResult('tc1'),
740+
makeToolResult('tc2', '["read_file", "edit_file"]'),
741+
];
742+
const result = stripToolSearchMessages(messages);
743+
expect(result).toHaveLength(3);
744+
const assistant = result[1];
745+
expect(assistant.role).toBe(Raw.ChatRole.Assistant);
746+
if (assistant.role === Raw.ChatRole.Assistant) {
747+
expect(assistant.toolCalls).toHaveLength(1);
748+
expect(assistant.toolCalls![0].id).toBe('tc1');
749+
}
750+
expect(result.find(m => m.role === Raw.ChatRole.Tool && m.toolCallId === 'tc2')).toBeUndefined();
751+
});
752+
753+
test('removes toolCalls property when all tool calls are tool_search', () => {
754+
const messages = [
755+
makeUserMessage(),
756+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search' }]),
757+
makeToolResult('tc1'),
758+
];
759+
const result = stripToolSearchMessages(messages);
760+
expect(result).toHaveLength(2);
761+
const assistant = result[1];
762+
if (assistant.role === Raw.ChatRole.Assistant) {
763+
expect(assistant.toolCalls).toBeUndefined();
764+
}
765+
});
766+
767+
test('does not strip server-side tool_search_tool_regex', () => {
768+
const messages = [
769+
makeUserMessage(),
770+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search_tool_regex' }]),
771+
makeToolResult('tc1'),
772+
];
773+
const result = stripToolSearchMessages(messages);
774+
expect(result).toBe(messages);
775+
});
776+
777+
test('preserves non-tool messages', () => {
778+
const messages = [
779+
makeUserMessage('first'),
780+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search' }]),
781+
makeToolResult('tc1'),
782+
makeUserMessage('second'),
783+
makeAssistantMessage([{ id: 'tc2', name: 'edit_file' }]),
784+
makeToolResult('tc2'),
785+
];
786+
const result = stripToolSearchMessages(messages);
787+
expect(result).toHaveLength(5);
788+
expect(result[0].content[0]).toEqual({ type: Raw.ChatCompletionContentPartKind.Text, text: 'first' });
789+
expect(result[2].content[0]).toEqual({ type: Raw.ChatCompletionContentPartKind.Text, text: 'second' });
790+
});
791+
});

0 commit comments

Comments
 (0)