Skip to content

Commit 7531e72

Browse files
sleitorfelixarntz
andauthored
fix(anthropic): handle encrypted_code_execution_result for multi-turn with web_fetch/web_search 20260209 (#13046)
## Problem When using `web_fetch_20260209` or `web_search_20260209`, the Anthropic API auto-injects a `code_execution` `server_tool_use` block in the response and returns its result as an `encrypted_code_execution_result`. The SDK previously only handled `code_execution_result` and `code_execution_tool_result_error` types, so the encrypted result was **silently dropped**. In multi-turn conversations, the absence of this `code_execution_tool_result` in the subsequent user message caused the Anthropic API to return: ``` messages.1: `code_execution` tool use with id `srvtoolu_01HopAaXMHo9To3RavUYwqy7` was found without a corresponding `code_execution_tool_result` block ``` Repro: use the example pages in `examples/ai-e2e-next`: - `/chat/anthropic-web-fetch-20260209` - `/chat/anthropic-web-search-20260209` Then send a follow-up message after a tool call was made in the previous response. Fixes #13040 ## Fix - **`anthropic-messages-language-model.ts`** (both streaming and non-streaming paths): when `code_execution_tool_result.content.type === 'encrypted_code_execution_result'`, emit a `tool-result` event carrying the encrypted payload so it is preserved in the conversation history - **`convert-to-anthropic-messages-prompt.ts`**: when reconstructing the user message for multi-turn, detect `encrypted_code_execution_result` output and emit the correct `code_execution_tool_result` block with the encrypted content (checked before the existing `code_execution_result` branch) - **`convert-to-anthropic-messages-prompt.test.ts`**: add a test covering the encrypted result round-trip in a multi-turn context --------- Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com> Co-authored-by: Felix Arntz <felix.arntz@vercel.com>
1 parent 2cd9553 commit 7531e72

File tree

7 files changed

+242
-5
lines changed

7 files changed

+242
-5
lines changed

.changeset/mighty-deers-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/anthropic': patch
3+
---
4+
5+
fix(provider/anthropic): handle encrypted_code_execution_result for multi-turn with web_fetch/web_search 20260209

examples/ai-e2e-next/app/chat/anthropic-programmatic-tool-calling/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { AnthropicProgrammaticToolCallingMessage } from '@/agent/anthropic/programmatic-tool-calling-agent';
44
import { Response } from '@/components/ai-elements/response';
55
import ChatInput from '@/components/chat-input';
6-
import AnthropicCodeExecutionView from '@/components/tool/anthropic-code-execution-view';
6+
import AnthropicCodeExecution20260120View from '@/components/tool/anthropic-code-execution-20260120-view';
77
import { useChat } from '@ai-sdk/react';
88
import { DefaultChatTransport } from 'ai';
99

@@ -41,7 +41,10 @@ export default function ChatAnthropicProgrammaticToolCalling() {
4141
}
4242
case 'tool-code_execution': {
4343
return (
44-
<AnthropicCodeExecutionView invocation={part} key={index} />
44+
<AnthropicCodeExecution20260120View
45+
invocation={part}
46+
key={index}
47+
/>
4548
);
4649
}
4750
case 'tool-rollDie': {

examples/ai-e2e-next/components/tool/anthropic-code-execution-20260120-view.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,58 @@ export default function AnthropicCodeExecution20260120View({
8787
)}
8888
</>
8989
)}
90+
{invocation.output.type === 'encrypted_code_execution_result' && (
91+
<>
92+
<span className="font-semibold">Encrypted Stdout:</span>
93+
<br />
94+
{invocation.output.encrypted_stdout}
95+
<br />
96+
{invocation.output.stderr && (
97+
<>
98+
<span className="font-semibold">Stderr:</span>
99+
<br />
100+
{invocation.output.stderr}
101+
<br />
102+
</>
103+
)}
104+
{invocation.output.content.length > 0 && (
105+
<div className="bg-gray-200 py-2 px-2 rounded-lg flex flex-col gap-1">
106+
<div className="px-1">
107+
<p className="text-black">
108+
{invocation.output.content.length > 1
109+
? 'downloads'
110+
: 'download'}
111+
</p>
112+
</div>
113+
{invocation.output.content.map(file => (
114+
<button
115+
className="bg-cyan-800 hover:bg-cyan-700 text-white rounded-lg py-1 px-2 border border-white cursor-pointer"
116+
key={file.file_id}
117+
onClick={() =>
118+
window.open(
119+
`/api/code-execution-files/anthropic/${file.file_id}`,
120+
'_blank',
121+
)
122+
}
123+
>
124+
<div className="flex gap-1 items-center justify-center">
125+
<Download />
126+
<p>{file.file_id}</p>
127+
</div>
128+
</button>
129+
))}
130+
</div>
131+
)}
132+
{invocation.output.return_code != null && (
133+
<>
134+
<span className="font-semibold">Return Code:</span>
135+
<br />
136+
{invocation.output.return_code}
137+
<br />
138+
</>
139+
)}
140+
</>
141+
)}
90142
{invocation.output.type === 'bash_code_execution_result' && (
91143
<>
92144
<span className="font-semibold">Stdout:</span>

packages/anthropic/src/anthropic-messages-language-model.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,19 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
10561056
content: part.content.content ?? [],
10571057
},
10581058
});
1059+
} else if (part.content.type === 'encrypted_code_execution_result') {
1060+
content.push({
1061+
type: 'tool-result',
1062+
toolCallId: part.tool_use_id,
1063+
toolName: toolNameMapping.toCustomToolName('code_execution'),
1064+
result: {
1065+
type: part.content.type,
1066+
encrypted_stdout: part.content.encrypted_stdout,
1067+
stderr: part.content.stderr,
1068+
return_code: part.content.return_code,
1069+
content: part.content.content ?? [],
1070+
},
1071+
});
10591072
} else if (part.content.type === 'code_execution_tool_result_error') {
10601073
content.push({
10611074
type: 'tool-result',
@@ -1622,6 +1635,22 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
16221635
content: part.content.content ?? [],
16231636
},
16241637
});
1638+
} else if (
1639+
part.content.type === 'encrypted_code_execution_result'
1640+
) {
1641+
controller.enqueue({
1642+
type: 'tool-result',
1643+
toolCallId: part.tool_use_id,
1644+
toolName:
1645+
toolNameMapping.toCustomToolName('code_execution'),
1646+
result: {
1647+
type: part.content.type,
1648+
encrypted_stdout: part.content.encrypted_stdout,
1649+
stderr: part.content.stderr,
1650+
return_code: part.content.return_code,
1651+
content: part.content.content ?? [],
1652+
},
1653+
});
16251654
} else if (
16261655
part.content.type === 'code_execution_tool_result_error'
16271656
) {

packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,88 @@ describe('assistant messages', () => {
17391739
`);
17401740
expect(warnings).toMatchInlineSnapshot(`[]`);
17411741
});
1742+
1743+
it('should pass back encrypted_code_execution_result for multi-turn (web_fetch_20260209/web_search_20260209)', async () => {
1744+
const warnings: SharedV3Warning[] = [];
1745+
const result = await convertToAnthropicMessagesPrompt({
1746+
prompt: [
1747+
{
1748+
role: 'assistant',
1749+
content: [
1750+
{
1751+
type: 'tool-call',
1752+
toolCallId: 'srvtoolu_webfetch_01',
1753+
toolName: 'web_fetch',
1754+
input: { url: 'https://example.com' },
1755+
providerExecuted: true,
1756+
},
1757+
{
1758+
type: 'tool-result',
1759+
toolCallId: 'srvtoolu_webfetch_01',
1760+
toolName: 'web_fetch',
1761+
output: {
1762+
type: 'json',
1763+
value: {
1764+
type: 'web_fetch_result',
1765+
url: 'https://example.com',
1766+
retrievedAt: '2026-01-01T00:00:00Z',
1767+
content: {
1768+
type: 'document',
1769+
title: 'Example',
1770+
source: {
1771+
type: 'text',
1772+
mediaType: 'text/plain',
1773+
data: 'hello',
1774+
},
1775+
},
1776+
},
1777+
},
1778+
},
1779+
{
1780+
type: 'tool-call',
1781+
toolCallId: 'srvtoolu_codeexec_01',
1782+
toolName: 'code_execution',
1783+
input: { code: 'print("done")' },
1784+
providerExecuted: true,
1785+
},
1786+
{
1787+
type: 'tool-result',
1788+
toolCallId: 'srvtoolu_codeexec_01',
1789+
toolName: 'code_execution',
1790+
output: {
1791+
type: 'json',
1792+
value: {
1793+
type: 'encrypted_code_execution_result',
1794+
encrypted_stdout: 'enc_abc123',
1795+
stderr: '',
1796+
return_code: 0,
1797+
content: [],
1798+
},
1799+
},
1800+
},
1801+
],
1802+
},
1803+
],
1804+
sendReasoning: false,
1805+
warnings,
1806+
toolNameMapping: defaultToolNameMapping,
1807+
});
1808+
1809+
const assistantMessage = result.prompt.messages[0];
1810+
expect(assistantMessage.role).toBe('assistant');
1811+
const codeExecResult = (assistantMessage.content as any[]).find(
1812+
(c: any) => c.type === 'code_execution_tool_result',
1813+
);
1814+
expect(codeExecResult).toBeDefined();
1815+
expect(codeExecResult.content).toEqual({
1816+
type: 'encrypted_code_execution_result',
1817+
encrypted_stdout: 'enc_abc123',
1818+
stderr: '',
1819+
return_code: 0,
1820+
content: [],
1821+
});
1822+
expect(warnings).toMatchInlineSnapshot(`[]`);
1823+
});
17421824
});
17431825

17441826
describe('code_execution 20250825', () => {

packages/anthropic/src/convert-to-anthropic-messages-prompt.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { anthropicFilePartProviderOptions } from './anthropic-messages-options';
2626
import { CacheControlValidator } from './get-cache-control';
2727
import { codeExecution_20250522OutputSchema } from './tool/code-execution_20250522';
2828
import { codeExecution_20250825OutputSchema } from './tool/code-execution_20250825';
29+
import { codeExecution_20260120OutputSchema } from './tool/code-execution_20260120';
2930
import { toolSearchRegex_20251119OutputSchema as toolSearchOutputSchema } from './tool/tool-search-regex_20251119';
3031
import { webFetch_20250910OutputSchema } from './tool/web-fetch-20250910';
3132
import { webSearch_20250305OutputSchema } from './tool/web-search_20250305';
@@ -767,8 +768,9 @@ export async function convertToAnthropicMessagesPrompt({
767768
break;
768769
}
769770

770-
// to distinguish between code execution 20250522 and 20250825,
771-
// we check if a type property is present in the output.value
771+
// to distinguish between code execution 20250522, 20250825,
772+
// and encrypted results (from web_fetch_20260209/web_search_20260209 injection),
773+
// we check the type property in output.value
772774
if (output.value.type === 'code_execution_result') {
773775
// code execution 20250522
774776
const codeExecutionOutput = await validateTypes({
@@ -788,6 +790,33 @@ export async function convertToAnthropicMessagesPrompt({
788790
},
789791
cache_control: cacheControl,
790792
});
793+
} else if (
794+
output.value.type === 'encrypted_code_execution_result'
795+
) {
796+
// code execution 20260120 encrypted result
797+
const codeExecutionOutput = await validateTypes({
798+
value: output.value,
799+
schema: codeExecution_20260120OutputSchema,
800+
});
801+
802+
if (
803+
codeExecutionOutput.type ===
804+
'encrypted_code_execution_result'
805+
) {
806+
anthropicContent.push({
807+
type: 'code_execution_tool_result',
808+
tool_use_id: part.toolCallId,
809+
content: {
810+
type: codeExecutionOutput.type,
811+
encrypted_stdout:
812+
codeExecutionOutput.encrypted_stdout,
813+
stderr: codeExecutionOutput.stderr,
814+
return_code: codeExecutionOutput.return_code,
815+
content: codeExecutionOutput.content ?? [],
816+
},
817+
cache_control: cacheControl,
818+
});
819+
}
791820
} else {
792821
// code execution 20250825
793822
const codeExecutionOutput = await validateTypes({
@@ -796,7 +825,6 @@ export async function convertToAnthropicMessagesPrompt({
796825
});
797826

798827
if (codeExecutionOutput.type === 'code_execution_result') {
799-
// Programmatic tool calling result - same format as 20250522
800828
anthropicContent.push({
801829
type: 'code_execution_tool_result',
802830
tool_use_id: part.toolCallId,

packages/anthropic/src/tool/code-execution_20260120.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ export const codeExecution_20260120OutputSchema = lazySchema(() =>
2323
.optional()
2424
.default([]),
2525
}),
26+
z.object({
27+
type: z.literal('encrypted_code_execution_result'),
28+
encrypted_stdout: z.string(),
29+
stderr: z.string(),
30+
return_code: z.number(),
31+
content: z
32+
.array(
33+
z.object({
34+
type: z.literal('code_execution_output'),
35+
file_id: z.string(),
36+
}),
37+
)
38+
.optional()
39+
.default([]),
40+
}),
2641
z.object({
2742
type: z.literal('bash_code_execution_result'),
2843
content: z.array(
@@ -183,6 +198,29 @@ const factory = createProviderToolFactoryWithOutputSchema<
183198
*/
184199
return_code: number;
185200

201+
/**
202+
* Output file Id list
203+
*/
204+
content: Array<{ type: 'code_execution_output'; file_id: string }>;
205+
}
206+
| {
207+
type: 'encrypted_code_execution_result';
208+
209+
/**
210+
* Encrypted output from successful execution
211+
*/
212+
encrypted_stdout: string;
213+
214+
/**
215+
* Error messages if execution fails
216+
*/
217+
stderr: string;
218+
219+
/**
220+
* 0 for success, non-zero for failure
221+
*/
222+
return_code: number;
223+
186224
/**
187225
* Output file Id list
188226
*/

0 commit comments

Comments
 (0)