豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 4b9c9b5

Browse files
authored
Merge pull request #4999 from microsoft/dev/bhavyau/fix-tool-reference-summarization-0.43
Fix: strip tool_search messages from summarization to prevent tool_reference errors
2 parents 303b0dc + a660818 commit 4b9c9b5

File tree

2 files changed

+149
-1
lines changed

2 files changed

+149
-1
lines changed

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';
@@ -714,6 +715,16 @@ class ConversationHistorySummarizer {
714715
stripCacheBreakpoints(summarizationPrompt);
715716

716717
let messages = ToolCallingLoop.stripInternalToolCallIds(summarizationPrompt);
718+
719+
// Strip custom client-side tool search (tool_search) tool_use/tool_result
720+
// pairs. The summarization call uses ChatLocation.Other but
721+
// createMessagesRequestBody still converts tool_search results to
722+
// tool_reference blocks (customToolSearchEnabled isn't gated by location).
723+
// Without tool search enabled in the request, Anthropic rejects them.
724+
if (isAnthropicFamily(endpoint)) {
725+
messages = stripToolSearchMessages(messages);
726+
}
727+
717728
// Gemini strictly requires every function_call to have a matching function_response.
718729
// When prompt-tsx prunes tool result messages due to token budget, orphaned tool_calls
719730
// can remain, causing a 400 INVALID_ARGUMENT error. Strip them for Gemini models.
@@ -914,6 +925,44 @@ function stripCacheBreakpoints(messages: ChatMessage[]): void {
914925
});
915926
}
916927

928+
/**
929+
* Strip custom client-side tool search (tool_search) tool_use and tool_result
930+
* messages from the conversation. The summarization call uses ChatLocation.Other
931+
* but createMessagesRequestBody still converts tool_search results to
932+
* tool_reference blocks (customToolSearchEnabled isn't gated by location).
933+
* Without tool search enabled in the request, Anthropic rejects tool_reference
934+
* content blocks with: "Input tag 'tool_reference' found using 'type' does not
935+
* match any of the expected tags".
936+
*/
937+
export function stripToolSearchMessages(messages: ChatMessage[]): ChatMessage[] {
938+
const toolSearchIds = new Set<string>();
939+
for (const message of messages) {
940+
if (message.role === Raw.ChatRole.Assistant && message.toolCalls) {
941+
for (const tc of message.toolCalls) {
942+
if (tc.function.name === CUSTOM_TOOL_SEARCH_NAME) {
943+
toolSearchIds.add(tc.id);
944+
}
945+
}
946+
}
947+
}
948+
949+
if (toolSearchIds.size === 0) {
950+
return messages;
951+
}
952+
953+
return messages.map(message => {
954+
if (message.role === Raw.ChatRole.Assistant && message.toolCalls) {
955+
const filteredToolCalls = message.toolCalls.filter(tc => !toolSearchIds.has(tc.id));
956+
if (filteredToolCalls.length !== message.toolCalls.length) {
957+
return { ...message, toolCalls: filteredToolCalls.length > 0 ? filteredToolCalls : undefined };
958+
}
959+
} else if (message.role === Raw.ChatRole.Tool && message.toolCallId && toolSearchIds.has(message.toolCallId)) {
960+
return undefined;
961+
}
962+
return message;
963+
}).filter((m): m is ChatMessage => m !== undefined);
964+
}
965+
917966
export interface ISummarizedConversationHistoryInfo {
918967
readonly props: SummarizedAgentHistoryProps;
919968
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;
@@ -737,3 +737,102 @@ suite('Inline Summarization Prompt', () => {
737737
expect(inlineMeta).toBeDefined();
738738
});
739739
});
740+
741+
suite('stripToolSearchMessages', () => {
742+
function makeAssistantMessage(toolCalls: { id: string; name: string }[], text = 'response'): Raw.ChatMessage {
743+
return {
744+
role: Raw.ChatRole.Assistant,
745+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
746+
toolCalls: toolCalls.map(tc => ({
747+
type: 'function' as const,
748+
id: tc.id,
749+
function: { name: tc.name, arguments: '{}' },
750+
})),
751+
};
752+
}
753+
754+
function makeToolResult(toolCallId: string, text = 'result'): Raw.ChatMessage {
755+
return {
756+
role: Raw.ChatRole.Tool,
757+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
758+
toolCallId,
759+
};
760+
}
761+
762+
function makeUserMessage(text = 'hello'): Raw.ChatMessage {
763+
return {
764+
role: Raw.ChatRole.User,
765+
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text }],
766+
};
767+
}
768+
769+
test('returns messages unchanged when no tool_search calls present', () => {
770+
const messages = [
771+
makeUserMessage(),
772+
makeAssistantMessage([{ id: 'tc1', name: 'read_file' }]),
773+
makeToolResult('tc1'),
774+
];
775+
const result = stripToolSearchMessages(messages);
776+
expect(result).toBe(messages);
777+
});
778+
779+
test('strips custom tool_search tool_use and tool_result', () => {
780+
const messages = [
781+
makeUserMessage(),
782+
makeAssistantMessage([
783+
{ id: 'tc1', name: 'read_file' },
784+
{ id: 'tc2', name: 'tool_search' },
785+
]),
786+
makeToolResult('tc1'),
787+
makeToolResult('tc2', '["read_file", "edit_file"]'),
788+
];
789+
const result = stripToolSearchMessages(messages);
790+
expect(result).toHaveLength(3);
791+
const assistant = result[1];
792+
expect(assistant.role).toBe(Raw.ChatRole.Assistant);
793+
if (assistant.role === Raw.ChatRole.Assistant) {
794+
expect(assistant.toolCalls).toHaveLength(1);
795+
expect(assistant.toolCalls![0].id).toBe('tc1');
796+
}
797+
expect(result.find(m => m.role === Raw.ChatRole.Tool && m.toolCallId === 'tc2')).toBeUndefined();
798+
});
799+
800+
test('removes toolCalls property when all tool calls are tool_search', () => {
801+
const messages = [
802+
makeUserMessage(),
803+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search' }]),
804+
makeToolResult('tc1'),
805+
];
806+
const result = stripToolSearchMessages(messages);
807+
expect(result).toHaveLength(2);
808+
const assistant = result[1];
809+
if (assistant.role === Raw.ChatRole.Assistant) {
810+
expect(assistant.toolCalls).toBeUndefined();
811+
}
812+
});
813+
814+
test('does not strip server-side tool_search_tool_regex', () => {
815+
const messages = [
816+
makeUserMessage(),
817+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search_tool_regex' }]),
818+
makeToolResult('tc1'),
819+
];
820+
const result = stripToolSearchMessages(messages);
821+
expect(result).toBe(messages);
822+
});
823+
824+
test('preserves non-tool messages', () => {
825+
const messages = [
826+
makeUserMessage('first'),
827+
makeAssistantMessage([{ id: 'tc1', name: 'tool_search' }]),
828+
makeToolResult('tc1'),
829+
makeUserMessage('second'),
830+
makeAssistantMessage([{ id: 'tc2', name: 'edit_file' }]),
831+
makeToolResult('tc2'),
832+
];
833+
const result = stripToolSearchMessages(messages);
834+
expect(result).toHaveLength(5);
835+
expect(result[0].content[0]).toEqual({ type: Raw.ChatCompletionContentPartKind.Text, text: 'first' });
836+
expect(result[2].content[0]).toEqual({ type: Raw.ChatCompletionContentPartKind.Text, text: 'second' });
837+
});
838+
});

0 commit comments

Comments
 (0)