diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 98c181f152..c7d7906c39 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -5,7 +5,7 @@ import logging import sys from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence -from typing import Any, ClassVar, Final, Generic, Literal, TypedDict +from typing import Any, ClassVar, Final, Generic, Literal, TypedDict, cast from agent_framework import ( Annotation, @@ -696,11 +696,42 @@ def _prepare_messages_for_anthropic(self, messages: Sequence[Message]) -> list[d This skips the first message if it is a system message, as Anthropic expects system instructions as a separate parameter. + + Anthropic's API requires that the conversation ends with a user message. + If the last message is from the assistant, a synthetic user turn is + appended when it will not break Anthropic tool_use/tool_result pairing. """ # first system message is passed as instructions if messages and isinstance(messages[0], Message) and messages[0].role == "system": - return [self._prepare_message_for_anthropic(msg) for msg in messages[1:]] - return [self._prepare_message_for_anthropic(msg) for msg in messages] + msgs = list(messages[1:]) + else: + msgs = list(messages) + + result = [self._prepare_message_for_anthropic(msg) for msg in msgs] + + # Anthropic requires the conversation to end with a user message. + # Append a synthetic user turn so chained agent outputs work as + # valid context for the next agent without rewriting the assistant message. + if result and result[-1].get("role") == "assistant" and not self._message_has_tool_use(result[-1]): + result.append({"role": "user", "content": "Continue"}) + + return result + + def _message_has_tool_use(self, message: dict[str, Any]) -> bool: + """Return whether an Anthropic message contains tool_use blocks.""" + content: object = message.get("content") + if not isinstance(content, list): + return False + + content_blocks = cast(list[object], content) + for content_block in content_blocks: + match content_block: + case {"type": "tool_use" | "mcp_tool_use" | "server_tool_use"}: + return True + case _: + pass + + return False def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]: """Prepare a Message for the Anthropic client. diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0cfec3423c..3e82ebb168 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -618,9 +618,37 @@ def test_prepare_messages_for_anthropic_without_system( result = client._prepare_messages_for_anthropic(messages) - assert len(result) == 2 + assert len(result) == 3 assert result[0]["role"] == "user" assert result[1]["role"] == "assistant" + assert result[2]["role"] == "user" + assert result[2]["content"] == "Continue" + + +def test_prepare_messages_for_anthropic_does_not_append_after_tool_use( + mock_anthropic_client: MagicMock, +) -> None: + """Do not append plain user text after assistant tool_use blocks.""" + client = create_test_anthropic_client(mock_anthropic_client) + messages = [ + Message(role="user", contents=["What's the weather?"]), + Message( + role="assistant", + contents=[ + Content.from_function_call( + call_id="call_123", + name="get_weather", + arguments={"location": "Seattle"}, + ) + ], + ), + ] + + result = client._prepare_messages_for_anthropic(messages) + + assert len(result) == 2 + assert result[1]["role"] == "assistant" + assert result[1]["content"][0]["type"] == "tool_use" # Tool Conversion Tests