From ae240d8df07354304fa8886117b483f7c4f3aaaf Mon Sep 17 00:00:00 2001 From: Praveen Mittal Date: Wed, 26 Aug 2026 23:00:08 +0200 Subject: [PATCH] python: preserve message roles in ClaudeAgent._format_prompt _format_prompt() joined every message's text with no role information, so in multi-agent orchestration (SequentialBuilder/ConcurrentBuilder) Claude couldn't tell its own prior turns apart from another agent's turns or the user's actual instruction. The Claude Agent SDK's streaming-input protocol only accepts user-role turns (it generates its own assistant turns), so per-message roles can't be sent on the wire. Instead, when the input is more than a single plain user turn, each message is now prefixed with its role as text (e.g. "[assistant]: ...") so Claude can still tell the turns apart. The common single-user-turn case is left unchanged. --- .../claude/agent_framework_claude/_agent.py | 12 +++++++++- .../claude/tests/test_claude_agent.py | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index e34f507c07f..e467052980c 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -733,6 +733,14 @@ async def _apply_runtime_options(self, client: ClaudeSDKClient, options: dict[st def _format_prompt(self, messages: list[Message] | None) -> str: """Format messages into a prompt string. + The Claude Agent SDK's streaming-input protocol only accepts ``user``-role + turns (it generates its own ``assistant`` turns), so a multi-message, + multi-role ``messages`` list can't be forwarded with real per-message + roles. Instead, when more than a single plain user turn is given (e.g. an + agent receiving prior turns from other agents and the user in a + multi-agent orchestration), each message is prefixed with its role so + Claude can still tell the turns apart from the text itself. + Args: messages: List of chat messages. @@ -741,7 +749,9 @@ def _format_prompt(self, messages: list[Message] | None) -> str: """ if not messages: return "" - return "\n".join([msg.text or "" for msg in messages]) + if len(messages) == 1 and messages[0].role == "user": + return messages[0].text or "" + return "\n".join(f"[{msg.role}]: {msg.text or ''}" for msg in messages) @property def default_options(self) -> dict[str, Any]: diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 38359f9d152..220e32841b0 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -1026,6 +1026,29 @@ def test_format_multiple_messages(self) -> None: assert "Hello!" in result assert "How are you?" in result + def test_format_multiple_messages_preserves_roles(self) -> None: + """Multi-message input (e.g. multi-agent orchestration history) must keep + each message's role visible in the text, since the Claude Agent SDK's + streaming-input protocol only accepts user-role turns and can't carry + per-message roles on the wire.""" + agent = ClaudeAgent() + messages = [ + Message(role="assistant", contents=[Content.from_text(text="Researcher: Paris is the capital.")]), + Message(role="user", contents=[Content.from_text(text="Critic: verify that.")]), + Message(role="assistant", contents=[Content.from_text(text="Researcher: confirmed.")]), + ] + result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] + assert "[assistant]: Researcher: Paris is the capital." in result + assert "[user]: Critic: verify that." in result + assert "[assistant]: Researcher: confirmed." in result + + def test_format_single_user_message_has_no_role_label(self) -> None: + """The common single-turn case is left unlabeled for backward compatibility.""" + agent = ClaudeAgent() + msg = Message(role="user", contents=[Content.from_text(text="Hello")]) + result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] + assert result == "Hello" + # region Test Build Options