diff --git a/.changeset/fix-prune-messages-caller-deps.md b/.changeset/fix-prune-messages-caller-deps.md new file mode 100644 index 000000000000..0682276e9ea3 --- /dev/null +++ b/.changeset/fix-prune-messages-caller-deps.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +Fix `pruneMessages` dropping tool calls referenced via provider-specific caller dependencies (e.g. Anthropic `code_execution` `caller.toolId`), causing "source tool not found" API errors. diff --git a/packages/ai/src/generate-text/prune-messages.test.ts b/packages/ai/src/generate-text/prune-messages.test.ts index 49d52b4c4d64..8b3db5e7edc4 100644 --- a/packages/ai/src/generate-text/prune-messages.test.ts +++ b/packages/ai/src/generate-text/prune-messages.test.ts @@ -716,5 +716,114 @@ describe('pruneMessages', () => { `); }); }); + + describe('caller tool id dependencies', () => { + it('should keep referenced caller tool calls when pruning', () => { + const messages: ModelMessage[] = [ + { + role: 'user', + content: [{ type: 'text', text: 'Use code execution.' }], + }, + // server_tool_use (provider-executed) + { + role: 'assistant', + content: [ + { + type: 'tool-call', + toolCallId: 'srvtoolu_ABC', + toolName: 'code_execution', + input: {}, + providerExecuted: true, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'srvtoolu_ABC', + toolName: 'code_execution', + output: { type: 'text', value: 'executed' }, + }, + ], + }, + // programmatic tool_use referencing the server_tool_use via caller + { + role: 'assistant', + content: [ + { + type: 'tool-call', + toolCallId: 'toolu_XYZ', + toolName: 'lookup', + input: { ticker: 'AAPL' }, + providerOptions: { + anthropic: { + caller: { + type: 'code_execution_20250825', + toolId: 'srvtoolu_ABC', + }, + }, + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'toolu_XYZ', + toolName: 'lookup', + output: { type: 'text', value: '185.42' }, + }, + ], + }, + // final assistant text + { + role: 'assistant', + content: [{ type: 'text', text: 'AAPL is $185.42.' }], + }, + // follow-up turn + { + role: 'user', + content: [{ type: 'text', text: 'Now check MSFT.' }], + }, + ]; + + const result = pruneMessages({ + messages, + toolCalls: 'before-last-5-messages', + emptyMessages: 'remove', + }); + + // srvtoolu_ABC must be kept because toolu_XYZ references it via caller + const allToolCallIds = result.flatMap(m => + typeof m.content === 'string' + ? [] + : m.content + .filter( + (p): p is { type: 'tool-call'; toolCallId: string } => + p.type === 'tool-call', + ) + .map(p => p.toolCallId), + ); + const allToolResultIds = result.flatMap(m => + typeof m.content === 'string' + ? [] + : m.content + .filter( + (p): p is { type: 'tool-result'; toolCallId: string } => + p.type === 'tool-result', + ) + .map(p => p.toolCallId), + ); + + expect(allToolCallIds).toContain('srvtoolu_ABC'); + expect(allToolCallIds).toContain('toolu_XYZ'); + expect(allToolResultIds).toContain('srvtoolu_ABC'); + expect(allToolResultIds).toContain('toolu_XYZ'); + }); + }); }); }); diff --git a/packages/ai/src/generate-text/prune-messages.ts b/packages/ai/src/generate-text/prune-messages.ts index 953cc3ea7211..0f0961597c57 100644 --- a/packages/ai/src/generate-text/prune-messages.ts +++ b/packages/ai/src/generate-text/prune-messages.ts @@ -98,6 +98,35 @@ export function pruneMessages({ } } } + + // Transitively keep tool calls referenced via provider-specific + // caller dependencies (e.g. Anthropic's code_execution caller.toolId). + let previousSize = 0; + while (keptToolCallIds.size !== previousSize) { + previousSize = keptToolCallIds.size; + for (const message of messages) { + if ( + message.role === 'assistant' && + typeof message.content !== 'string' + ) { + for (const part of message.content) { + if ( + part.type === 'tool-call' && + keptToolCallIds.has(part.toolCallId) + ) { + const callerToolId = ( + part.providerOptions?.anthropic as + | { caller?: { toolId?: string } } + | undefined + )?.caller?.toolId; + if (callerToolId) { + keptToolCallIds.add(callerToolId); + } + } + } + } + } + } } messages = messages.map((message, messageIndex) => {