Every one of these cost real time to learn. None of them is difficult once somebody has said it out loud.
Rule 1 β Append the whole reply, not just the text
// β throws away the tool_use block
const text = response.content.find(b => b.type === "text").text;
messages.push({ role: "assistant", content: text });
// β
messages.push({ role: "assistant", content: response.content });response.content is a list of blocks: text, thinking, and tool_use.
Extract just the text and the tool_use block vanishes β so your tool_result
refers to an id the API has never heard of, and it rejects the whole request
with a message about a mismatched tool_use_id.
Rule 2 β All tool results go back in ONE user message
If the model asks for three tools at once, send one user message containing
three tool_result blocks.
The failure mode here is the nasty kind: nothing breaks. Your agent still works. It is just slower and more expensive than it should be, forever, and no test will ever tell you.
Rule 3 β Cap the loop, hard
const MAX_ITERATIONS = 10;An unbounded tool-calling loop is not a hang. It's a bill.
That is the whole rule, and it is worth being precise about why it is worse than an ordinary infinite loop. A normal runaway loop burns CPU you have already paid for. This one makes a paid API call every time round, with a conversation that is larger every time.
Two more things belong in the same seatbelt:
Handle every stop_reason explicitly. end_turn, max_tokens, refusal,
pause_turn each get their own branch. A loop that only asks "is it
tool_use?" is a loop that can spin on a case you didn't think about.
Put the token count in the UI, per iteration. Partly a feature. Mostly a seatbelt. You cannot manage a cost you cannot see.
Where the secret lives
Not a rule so much as a fact with no workaround.
A browser cannot keep a secret. If ANTHROPIC_API_KEY appears anywhere in
a client component it is compiled into the JavaScript bundle, served to every
visitor, and scraped within hours.
There is no clever client-side workaround. The same rule covers
MCP_SHARED_TOKEN and, in project #3, DATABASE_URL β a connection string
that reaches the browser bundle is a public database.