From ea6f08b2c7d35be6e15f8dc2398fc192e7053231 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 4 Sep 2026 14:13:33 +0200 Subject: [PATCH 1/3] Python: preserve authors in Claude prompts --- .../claude/agent_framework_claude/_agent.py | 4 ++- .../claude/tests/test_claude_agent.py | 25 +++++++++++++++++++ 2 files changed, 28 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..d9f687b92c4 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -741,7 +741,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: + return messages[0].text + return "\n".join(f"[{m.author_name or m.role}]: {m.text or ''}" for m 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..b54e406b65c 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -1011,6 +1011,7 @@ def test_format_user_message(self) -> None: contents=[Content.from_text(text="Hello")], ) result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] + assert "[user]:" not in result assert "Hello" in result def test_format_multiple_messages(self) -> None: @@ -1025,6 +1026,30 @@ def test_format_multiple_messages(self) -> None: assert "Hi" in result assert "Hello!" in result assert "How are you?" in result + assert "[assistant]:" in result + assert result.count("[user]:") == 2 + + def test_format_messages_from_other_agent(self) -> None: + """Test formatting messages from two agents in sequence.""" + # SequenceBuilder, will pass on different roles, semantically speaking the + # only useful information we can maintain is the `author_name` of the message + # which is attributed to the source agent. Failing that we include the role + agent = ClaudeAgent() + messages = [ + Message( + role="assistant", + author_name="previous_agent", + contents=[Content.from_text(text="Hello from previous agent")], + ), + Message(role="assistant", contents=[Content.from_text(text="Hello from a nameless author")]), + ] + result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] + + assert "user" not in result # claudeSDKClient.query is a hardcoded `user` message + assert "[previous_agent]:" in result + assert "Hello from previous agent" in result + assert "[assistant]:" in result + assert "Hello from a nameless author" in result # region Test Build Options From ef5fc035173a01004982f0980e512997dcf832a6 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Mon, 7 Sep 2026 13:31:16 +0200 Subject: [PATCH 2/3] Added preamble prompt, made ClaudeAgent SupportsAgentRun compliant, sample --- python/.vscode/settings.json | 8 ++- .../claude/agent_framework_claude/_agent.py | 22 ++++---- .../claude/tests/test_claude_agent.py | 47 +++++++++++------ .../02-agents/providers/anthropic/README.md | 1 + .../anthropic_claude_sequential_agents.py | 52 +++++++++++++++++++ 5 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py diff --git a/python/.vscode/settings.json b/python/.vscode/settings.json index 181b926ac0e..95f7b6f76c0 100644 --- a/python/.vscode/settings.json +++ b/python/.vscode/settings.json @@ -35,5 +35,11 @@ "name": "azure", "depth": 2 } - ] + ], + "python.testing.pytestArgs": [ + "--import-mode=importlib", + "packages" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index d9f687b92c4..14f7d2a871b 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -741,9 +741,13 @@ def _format_prompt(self, messages: list[Message] | None) -> str: """ if not messages: return "" - if len(messages) == 1: - return messages[0].text - return "\n".join(f"[{m.author_name or m.role}]: {m.text or ''}" for m in messages) + if len(messages) == 1 and messages[0].role == "user": + return messages[0].text or "" + prefix = "The following is conversation history supplied to this agent.\n" + prefix += "Each label identifies the original speaker's role.\n" + prefix += "Use this history as context for your assigned task.\n" + + return prefix + "\n".join(f"[{m.role}]: {m.text or ''}" for m in messages) @property def default_options(self) -> dict[str, Any]: @@ -1028,8 +1032,8 @@ def run( tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]]: ... @@ -1045,8 +1049,8 @@ def run( tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... @@ -1061,8 +1065,8 @@ def run( # pyright: ignore[reportIncompatibleMethodOverride] tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """Run the Claude agent with telemetry enabled.""" diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index b54e406b65c..1caa157d82d 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -1011,8 +1011,7 @@ def test_format_user_message(self) -> None: contents=[Content.from_text(text="Hello")], ) result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] - assert "[user]:" not in result - assert "Hello" in result + assert result == "Hello" def test_format_multiple_messages(self) -> None: """Test formatting multiple messages.""" @@ -1023,17 +1022,17 @@ def test_format_multiple_messages(self) -> None: Message(role="user", contents=[Content.from_text(text="How are you?")]), ] result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] - assert "Hi" in result - assert "Hello!" in result - assert "How are you?" in result - assert "[assistant]:" in result - assert result.count("[user]:") == 2 + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[user]: Hi\n" + "[assistant]: Hello!\n" + "[user]: How are you?" + ) def test_format_messages_from_other_agent(self) -> None: - """Test formatting messages from two agents in sequence.""" - # SequenceBuilder, will pass on different roles, semantically speaking the - # only useful information we can maintain is the `author_name` of the message - # which is attributed to the source agent. Failing that we include the role + """Test that author names do not replace roles in handed-over history.""" agent = ClaudeAgent() messages = [ Message( @@ -1044,12 +1043,28 @@ def test_format_messages_from_other_agent(self) -> None: Message(role="assistant", contents=[Content.from_text(text="Hello from a nameless author")]), ] result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[assistant]: Hello from previous agent\n" + "[assistant]: Hello from a nameless author" + ) - assert "user" not in result # claudeSDKClient.query is a hardcoded `user` message - assert "[previous_agent]:" in result - assert "Hello from previous agent" in result - assert "[assistant]:" in result - assert "Hello from a nameless author" in result + def test_format_single_assistant_message(self) -> None: + """Test formatting a single assistant message.""" + agent = ClaudeAgent() + msg = Message( + role="assistant", + contents=[Content.from_text(text="Hello from assistant")], + ) + result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[assistant]: Hello from assistant" + ) # region Test Build Options diff --git a/python/samples/02-agents/providers/anthropic/README.md b/python/samples/02-agents/providers/anthropic/README.md index 0db183490a2..f1857624c3e 100644 --- a/python/samples/02-agents/providers/anthropic/README.md +++ b/python/samples/02-agents/providers/anthropic/README.md @@ -16,6 +16,7 @@ This folder contains examples demonstrating how to use Anthropic's Claude models | File | Description | |------|-------------| | [`anthropic_claude_basic.py`](anthropic_claude_basic.py) | Basic usage of ClaudeAgent with streaming, non-streaming, and custom tools. | +| [`anthropic_claude_sequential_agents.py`](anthropic_claude_sequential_agents.py) | Uses SequentialBuilder to pass role-labeled conversation history from a grammar inspector to a second Claude agent. | | [`anthropic_claude_with_tools.py`](anthropic_claude_with_tools.py) | Using built-in tools (Read, Glob, Grep, etc.). | | [`anthropic_claude_with_shell.py`](anthropic_claude_with_shell.py) | Shell command execution with interactive permission handling. | | [`anthropic_claude_with_multiple_permissions.py`](anthropic_claude_with_multiple_permissions.py) | Combining multiple tools (Bash, Read, Write) with permission prompts. | diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py new file mode 100644 index 00000000000..bf6f41e8194 --- /dev/null +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import AgentResponse +from agent_framework_claude import ClaudeAgent +from agent_framework_orchestrations import SequentialBuilder +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +async def main() -> None: + """ + Anthropic Claude Sequential Agents Example + + Demonstrate conversation history handover between two Claude agents. + + SequentialBuilder passes the original user message and the grammar inspector's + response to the second agent. ClaudeAgent represents that history as a transcript + with role labels inside a single SDK user message, rather than resuming a shared + Claude session. + """ + agents = [ + ClaudeAgent( + instructions="You are an agent that corrects English grammar mistakes.", + name="grammar_inspector" + ), + ClaudeAgent( + instructions="You are an agent that lists the diff between the participants of this conversation", + name="diff_highlighter" + ) + ] + workflow = SequentialBuilder( + participants=agents, + output_from="all" + ).build() + + prompt = "Yesterday she go to the store and buyed two apple." + result = await workflow.run(prompt) + + print(f"[user]\n{prompt}") + for response in result.get_outputs(): + if isinstance(response, AgentResponse): + for message in response.messages: + author = message.author_name or message.role + print(f"\n[{author}]\n{message.text}") + + +if __name__ == "__main__": + asyncio.run(main()) From 345c5eb86b996fe26d6070fa2f23ae5d47d92cd0 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Mon, 7 Sep 2026 14:19:59 +0200 Subject: [PATCH 3/3] Reverted settings.json file changes --- python/.vscode/settings.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/python/.vscode/settings.json b/python/.vscode/settings.json index 95f7b6f76c0..181b926ac0e 100644 --- a/python/.vscode/settings.json +++ b/python/.vscode/settings.json @@ -35,11 +35,5 @@ "name": "azure", "depth": 2 } - ], - "python.testing.pytestArgs": [ - "--import-mode=importlib", - "packages" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + ] }