Skip to content

Python: [Bug]: HandoffBuilder with AzureOpenAIChatClient crashes with "Invalid type for messages[N].content: expected string, got object" #4052

@frdeange

Description

@frdeange

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:

  1. Create a HandoffBuilder workflow with AzureOpenAIChatClient and multiple agents
  2. Configure autonomous mode with .with_autonomous_mode()
  3. 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

agent orchestrationIssues related to agent orchestrationpythonv1.0Features being tracked for the version 1.0 GA

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions