Summary
Two related bugs when using client.beta.messages.toolRunner with code_execution_20260120 and client-side tools (betaTool):
container.id is not automatically forwarded from one iteration to the next, causing a hard error when client-side tool results are sent back.
- Using
setMessagesParams to manually set container.id as a workaround causes the model to enter an infinite duplicate tool-calling loop.
Environment
@anthropic-ai/sdk version: 0.80.0
- Node.js version: v22.14.0
- Model:
claude-opus-4-6 or claude-sonnet-4-6
Reproduction
import "dotenv/config";
import Anthropic from "@anthropic-ai/sdk";
import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";
const client = new Anthropic();
const calculateSumTool = betaTool({
name: "calculate_sum",
description: "Add two numbers together",
inputSchema: {
type: "object",
properties: {
a: { type: "number", description: "First number" },
b: { type: "number", description: "Second number" },
},
required: ["a", "b"],
},
run: async (input) => String(input.a + input.b),
});
const runner = client.beta.messages.toolRunner({
model: "claude-opus-4-6",
max_tokens: 1024,
tools: [
{ ...calculateSumTool, allowed_callers: ["code_execution_20260120"] },
{ type: "code_execution_20260120", name: "code_execution" },
],
messages: [{ role: "user", content: "What's the sum of 5 and 3?" }],
stream: true,
});
for await (const messageStream of runner) {
for await (const event of messageStream) {}
const iterationMessage = await messageStream.finalMessage();
console.log("Container: ", iterationMessage.container);
console.log("Output: ", iterationMessage.content);
const toolResult = await runner.generateToolResponse();
console.log("Tool Result", toolResult);
// could be a tmp or permanent fix -> causes infinite tool call loop
if (iterationMessage.container?.id) {
runner.setMessagesParams((params) => ({
...params,
container: iterationMessage.container!.id, // passing string directly
}));
}
}
Bug 1: Missing container.id propagation
After iteration A completes, iterationMessage.container contains a valid container.id. However, when runner.generateToolResponse() triggers the next request, the container.id is not included, causing:
{
"type": "invalid_request_error",
"message": "container_id is required when there are pending tool uses generated by code execution with tools."
}
Expected behavior: toolRunner should automatically carry container.id from the previous iteration's response into the next request, the same way it already stitches tool_use / tool_result message pairs.
Bug 2: setMessagesParams workaround — two failure modes
Since the runner doesn't handle this automatically, the only recourse is setMessagesParams. Both available approaches are broken:
Approach A — pass an object:
runner.setMessagesParams((params) => ({
...params,
container: {
id: iterationMessage.container?.id,
skills: [],
},
}));
Fails immediately with:
{
"type": "invalid_request_error",
"message": "container: Input should be a valid string"
}
This is surprising — the container.id logged just before the call is a valid string (e.g. container_011CZZfD7NfLQumhoWHGwYmw), yet the API rejects it. It's unclear whether setMessagesParams is serializing the field incorrectly, or whether the API's container field type contract differs from the SDK's TypeScript types.
Approach B — pass the string directly:
runner.setMessagesParams((params) => ({
...params,
container: iterationMessage.container!.id,
}));
Does not error, but causes an infinite tool-calling loop. The model repeatedly calls code_execution → calculate_sum with identical inputs and near-identical outputs, never terminating:
Container: { id: 'container_011CZZfD7NfLQumhoWHGwYmw', expires_at: '...' }
Output: [ { type: 'server_tool_use', name: 'code_execution', input: { code: '...calculate_sum({"a": 5, "b": 3})...' } },
{ type: 'tool_use', name: 'calculate_sum', input: { a: 5, b: 3 } } ]
Tool Result { role: 'user', content: [{ type: 'tool_result', tool_use_id: '...', content: '8' }] }
DEBUG: container_011CZZfD7NfLQumhoWHGwYmw
// ... repeats indefinitely with new tool IDs but identical inputs/outputs
This strongly suggests that setMessagesParams is corrupting or resetting the accumulated message history between iterations, causing the model to lose context of what it has already executed.
Additional context
The two bugs compound each other completely: Bug 1 makes container.id forwarding mandatory for this use case, and Bug 2 means both available workarounds either hard-error or loop infinitely. There is currently no working path to use client-side betaTools alongside code_execution_20260120 in a streaming toolRunner loop.
Summary
Two related bugs when using
client.beta.messages.toolRunnerwithcode_execution_20260120and client-side tools (betaTool):container.idis not automatically forwarded from one iteration to the next, causing a hard error when client-side tool results are sent back.setMessagesParamsto manually setcontainer.idas a workaround causes the model to enter an infinite duplicate tool-calling loop.Environment
@anthropic-ai/sdkversion: 0.80.0claude-opus-4-6orclaude-sonnet-4-6Reproduction
Bug 1: Missing
container.idpropagationAfter iteration A completes,
iterationMessage.containercontains a validcontainer.id. However, whenrunner.generateToolResponse()triggers the next request, thecontainer.idis not included, causing:{ "type": "invalid_request_error", "message": "container_id is required when there are pending tool uses generated by code execution with tools." }Expected behavior:
toolRunnershould automatically carrycontainer.idfrom the previous iteration's response into the next request, the same way it already stitchestool_use/tool_resultmessage pairs.Bug 2:
setMessagesParamsworkaround — two failure modesSince the runner doesn't handle this automatically, the only recourse is
setMessagesParams. Both available approaches are broken:Approach A — pass an object:
Fails immediately with:
{ "type": "invalid_request_error", "message": "container: Input should be a valid string" }This is surprising — the
container.idlogged just before the call is a valid string (e.g.container_011CZZfD7NfLQumhoWHGwYmw), yet the API rejects it. It's unclear whethersetMessagesParamsis serializing the field incorrectly, or whether the API'scontainerfield type contract differs from the SDK's TypeScript types.Approach B — pass the string directly:
Does not error, but causes an infinite tool-calling loop. The model repeatedly calls
code_execution→calculate_sumwith identical inputs and near-identical outputs, never terminating:This strongly suggests that
setMessagesParamsis corrupting or resetting the accumulated message history between iterations, causing the model to lose context of what it has already executed.Additional context
The two bugs compound each other completely: Bug 1 makes
container.idforwarding mandatory for this use case, and Bug 2 means both available workarounds either hard-error or loop infinitely. There is currently no working path to use client-sidebetaTools alongsidecode_execution_20260120in a streamingtoolRunnerloop.