-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Description
When using HandoffBuilder with AzureOpenAIChatClient in autonomous mode, the workflow crashes after the first handoff (e.g., research_agent hands off back to coordinator). The OpenAI Chat Completions API rejects the request because messages[N].content contains a Python dict object instead of a string.
What happened:
The handoff tool's function_result content contains a dict like {"handoff_to": "research_agent"} as content.result. When _prepare_message_for_openai() in _chat_client.py processes this message, it assigns content.result directly as the message's content field without serializing it to a string. The Chat Completions API requires content to be either a string or an array of content objects, so it rejects the dict.
What I expected:
The handoff workflow should complete successfully, with agents handing off control between each other without API errors.
Steps to reproduce:
- Create a
HandoffBuilderworkflow withAzureOpenAIChatClientand multiple agents - Configure autonomous mode with
.with_autonomous_mode() - Run the workflow — it crashes after the first agent hands off back to the coordinator
Code Sample
import asyncio
from agent_framework import Agent, AgentResponseUpdate, Message, resolve_agent_id
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential
async def main():
client = AzureOpenAIChatClient(credential=AzureCliCredential())
coordinator = client.as_agent(
instructions="You are a coordinator. Route tasks to specialists.",
name="coordinator",
)
research_agent = client.as_agent(
instructions="You are a research specialist. Research the topic, then return control to coordinator.",
name="research_agent",
)
summary_agent = client.as_agent(
instructions="You summarize research findings. When done, return control to coordinator.",
name="summary_agent",
)
workflow = (
HandoffBuilder(
name="test_handoff",
participants=[coordinator, research_agent, summary_agent],
termination_condition=lambda conv: (
sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 3
),
)
.with_start_agent(coordinator)
.add_handoff(coordinator, [research_agent, summary_agent])
.add_handoff(research_agent, [coordinator])
.add_handoff(summary_agent, [coordinator])
.with_autonomous_mode(
turn_limits={
resolve_agent_id(coordinator): 3,
resolve_agent_id(research_agent): 3,
resolve_agent_id(summary_agent): 3,
}
)
.build()
)
async for event in workflow.run("Research Microsoft Agent Framework.", stream=True):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate) and event.data.text:
print(event.data.text, end="", flush=True)
asyncio.run(main())Error Messages / Stack Traces
openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid type for 'messages[3].content': expected one of a string or array of objects, but got an object instead.", 'type': 'invalid_request_error', 'param': 'messages[3].content', 'code': None}}
Full traceback points to _chat_client.py → _prepare_message_for_openai():
File ".../agent_framework/openai/_chat_client.py", line 558, in _prepare_message_for_openai
args["content"] = content.result if content.result is not None else ""
Root Cause
In _chat_client.py, the _prepare_message_for_openai() method handles function_result content type at approximately line 558:
case "function_result":
args["tool_call_id"] = content.call_id
args["content"] = content.result if content.result is not None else ""The handoff middleware (_AutoHandoffMiddleware) produces a synthetic function_result where content.result is a dict (e.g., {"handoff_to": "research_agent"}), not a string. This dict is passed directly as the message content, but the OpenAI Chat Completions API requires content to be a string for tool result messages.
Suggested Fix
Serialize non-string results with json.dumps():
case "function_result":
args["tool_call_id"] = content.call_id
result = content.result
if result is None:
args["content"] = ""
elif isinstance(result, str):
args["content"] = result
else:
import json
args["content"] = json.dumps(result)Package Versions
agent-framework: 1.0.0b260212, agent-framework-orchestrations: 1.0.0b260212
Python Version
Python 3.13.9
Additional Context
This issue is related to previously reported issues #3097 and #3713. Those issues were closed, but the underlying bug in _chat_client.py was not fixed — only the recommendation to use AzureOpenAIResponsesClient was given. However, since AzureOpenAIChatClient is still a supported client and the HandoffBuilder documentation does not restrict usage to the Responses client, this should be fixed.
The AzureOpenAIResponsesClient has its own separate bugs with HandoffBuilder — see companion issue.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status