-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Description
What happened?
When using workflow.as_agent() with a HandoffBuilder workflow, all messages from the conversation (including user input and agent responses) are concatenated into a single message with author_name='handoff-coordinator'.
The user's input text appears at the beginning of the assistant's response, making it impossible to distinguish between user messages and agent responses.
Example output:
Input: "I want to request sick leave"
Output: "I want to request sick leaveOf course! I can help you with that..."
What did you expect to happen?
The AgentRunResponse.messages should contain separate ChatMessage objects preserving:
- Individual
role(user vs assistant) - Individual
author_name(which agent responded) - Separate
textcontent for each message
This works correctly when using workflow.run().get_outputs() directly, but NOT when using workflow.as_agent().run().
Steps to reproduce the issue
- Create a handoff workflow with a coordinator and specialist agents
- Call
workflow.as_agent()to wrap it as an agent - Call
agent.run(messages=[...]) - Observe that
response.messagescontains a single message with all content concatenated
Code Sample
import asyncio
from agent_framework import ChatMessage, HandoffBuilder, ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
async def reproduce_bug():
client = AzureOpenAIChatClient(
endpoint="YOUR_ENDPOINT",
deployment_name="YOUR_MODEL",
api_key="YOUR_KEY"
)
coordinator = ChatAgent(
chat_client=client,
name="Coordinator",
system_prompt="Route to SpecialistAgent for any request."
)
specialist = ChatAgent(
chat_client=client,
name="SpecialistAgent",
system_prompt="You help with requests. Respond briefly."
)
workflow = (
HandoffBuilder(name="Test", participants=[coordinator, specialist])
.set_coordinator(coordinator)
.add_handoff(coordinator, [specialist])
.with_interaction_mode(interaction_mode="autonomous", autonomous_turn_limit=2)
.build()
)
# BUG: Using as_agent() concatenates everything
agent = workflow.as_agent(name="TestAgent")
response = await agent.run(messages=[ChatMessage(role="user", text="Hello")])
print(f"Number of messages: {len(response.messages)}")
for msg in response.messages:
print(f"Author: {msg.author_name}, Role: {msg.role}")
print(f"Text: {msg.text[:100]}...") # Shows "Hello" + agent response concatenated
# WORKS CORRECTLY: Using workflow.run() directly
result = await workflow.run(message=[ChatMessage(role="user", text="Hello")])
outputs = result.get_outputs()
print(f"\nDirect workflow outputs: {len(outputs)} separate items")
asyncio.run(reproduce_bug())Error Messages / Stack Traces
No error/exception is raised. The bug is in the behavior:
Using as_agent().run():
- response.messages = [ChatMessage(author='handoff-coordinator', text='HelloHi! How can I help?')]
^ User input "Hello" concatenated with assistant response
Using workflow.run().get_outputs():
- outputs = [ChatMessage(role='user', text='Hello'), ChatMessage(role='assistant', author='SpecialistAgent', text='Hi! How can I help?')]
^ Correctly separated messagesPackage Versions
agent-framework: 1.0.0b260107
Python Version
Python 3.12
Additional Context
Root Cause Analysis
The bug is in WorkflowAgent._convert_workflow_event_to_agent_update() in _agent.py (around line 315-350).
When HandoffCoordinator emits yield_output(list[ChatMessage]), the method:
- Does NOT recognize
list[ChatMessage]as a special case - Falls through to
_extract_contents(data)which flattens all contents from all messages into a single list - Creates ONE
AgentRunResponseUpdatewith all contents combined andauthor_name='handoff-coordinator'
Current problematic code:
case WorkflowOutputEvent(data=data, source_executor_id=source_executor_id):
if isinstance(data, ChatMessage): # Only handles single ChatMessage
return AgentRunResponseUpdate(...)
# When data is list[ChatMessage], it falls here:
contents = self._extract_contents(data) # Flattens everything!
return AgentRunResponseUpdate(
contents=contents, # All contents in ONE update
author_name=source_executor_id, # 'handoff-coordinator'
...
)Suggested fix:
Add handling for list[ChatMessage] before the fallback:
case WorkflowOutputEvent(data=data, source_executor_id=source_executor_id):
# ... existing checks ...
if isinstance(data, ChatMessage):
return AgentRunResponseUpdate(...)
# NEW: Handle list of ChatMessage - yield multiple updates
if isinstance(data, list) and all(isinstance(m, ChatMessage) for m in data):
# Return updates for each message separately, or handle differently
# This needs design decision on how to yield multiple updates
pass
contents = self._extract_contents(data) # FallbackWorkaround
Use workflow.run() directly instead of workflow.as_agent().run() and manage threads manually. This preserves message boundaries but loses the convenient thread/memory support of WorkflowAgent.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status