Skip to content

Python: [Bug]: WorkflowAgent.as_agent() concatenates all messages into one, losing message boundaries in handoff workflows #3244

@frdeange

Description

@frdeange

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 text content 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

  1. Create a handoff workflow with a coordinator and specialist agents
  2. Call workflow.as_agent() to wrap it as an agent
  3. Call agent.run(messages=[...])
  4. Observe that response.messages contains 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 messages

Package 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:

  1. Does NOT recognize list[ChatMessage] as a special case
  2. Falls through to _extract_contents(data) which flattens all contents from all messages into a single list
  3. Creates ONE AgentRunResponseUpdate with all contents combined and author_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)  # Fallback

Workaround

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

bugSomething isn't workingpythonv1.0Features being tracked for the version 1.0 GA

Type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions