Skip to content

Python: [Bug]: AzureAIClient agent_name Persists Across ChatAgent Instances #3446

@cicorias

Description

@cicorias

Description

Summary

When sharing a single AzureAIClient instance across multiple ChatAgent instances in a workflow, the agent_name from the first agent persists and is used for all subsequent agents. This causes tools to not be called correctly by subsequent agents in the workflow chain.

Affected Package

  • Package: agent-framework-azure-ai
  • Version: 1.0.0b260123 (and likely earlier versions)
  • File: agent_framework_azure_ai/_chat_client.py
  • Lines: 1233-1245

Related GitHub Issues

No existing issues found for this specific bug. Related issues reviewed:

Bug Description

The AzureAIChatClient._update_agent_name_and_description() method only sets the agent_name if one doesn't already exist:

# File: agent_framework_azure_ai/_chat_client.py, lines 1233-1245
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None) -> None:
    """Update the agent name in the chat client.

    Args:
        agent_name: The new name for the agent.
        description: The new description for the agent.
    """
    # This is a no-op in the base class, but can be overridden by subclasses
    # to update the agent name in the client.
    if agent_name and not self.agent_name:  # <-- BUG: Only sets if not already set
        self.agent_name = agent_name
    if description and not self.agent_description:
        self.agent_description = description

This method is called by ChatAgent.__init__() in agent_framework/_agents.py:

# File: agent_framework/_agents.py, lines 732-742
def _update_agent_name_and_description(self) -> None:
    """Update the agent name in the chat client."""
    if hasattr(self.chat_client, "_update_agent_name_and_description") and callable(
        self.chat_client._update_agent_name_and_description
    ):
        self.chat_client._update_agent_name_and_description(self.name, self.description)

Root Cause

When multiple ChatAgent instances share the same AzureAIClient:

  1. The first ChatAgent sets client.agent_name to its name
  2. Subsequent ChatAgent instances cannot override this value
  3. All API requests use the first agent's name in the extra_json payload

Impact

  • Severity: High - Breaks tool calling in multi-executor workflows
  • Symptom: MCP tools are connected but never invoked
  • Model behavior: Instead of calling tools, the model outputs text "simulating" tool calls

When the wrong agent name is sent in the API request, the model may not recognize the available tools, causing it to generate text describing what it would do rather than actually invoking the tools.

Reproduction

Minimal Reproduction Code

"""Minimal reproduction of AzureAIClient agent_name persistence bug."""
import asyncio
import os

from agent_framework import ChatAgent, ChatMessage
from agent_framework_azure_ai import AzureAIClient
from azure.identity.aio import DefaultAzureCredential


async def main():
    # Create a SHARED client (common pattern in workflows)
    shared_client = AzureAIClient(
        project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
        model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
        credential=DefaultAzureCredential(),
    )
    
    # First agent sets the agent_name
    async with ChatAgent(
        chat_client=shared_client,
        name="first-agent",  # This name PERSISTS
        instructions="You are the first agent.",
    ) as agent1:
        await agent1.run("Hello")
    
    # Second agent SHOULD use "second-agent" but uses "first-agent"
    async with ChatAgent(
        chat_client=shared_client,
        name="second-agent",  # This name is IGNORED!
        instructions="You are the second agent.",
    ) as agent2:
        # Enable debug logging to see the issue
        # The API request will show: 'agent': {'name': 'first-agent'}
        # instead of: 'agent': {'name': 'second-agent'}
        await agent2.run("Hello")

    # Verify the bug
    print(f"Client agent_name: {shared_client.agent_name}")  # Prints: first-agent
    # Expected: second-agent (or dynamically updated per agent)


if __name__ == "__main__":
    asyncio.run(main())

Workflow Reproduction (Real-World Scenario)

"""Real-world reproduction: Multi-executor workflow with MCP tools."""
import asyncio
import os

from agent_framework import (
    ChatAgent,
    ChatMessage,
    Executor,
    Workflow,
    WorkflowBuilder,
    WorkflowContext,
    handler,
)
from agent_framework._mcp import MCPStreamableHTTPTool
from agent_framework_azure_ai import AzureAIClient
from azure.identity.aio import DefaultAzureCredential


class FirstExecutor(Executor):
    """First executor - sets the agent name in shared client."""
    
    def __init__(self, chat_client: AzureAIClient):
        self.chat_client = chat_client
        super().__init__(id="first_executor")
    
    @handler
    async def handle(self, message: ChatMessage, ctx: WorkflowContext[dict]) -> None:
        async with ChatAgent(
            chat_client=self.chat_client,
            name="first-agent",  # This name persists in the client
            instructions="Parse the input message.",
        ) as agent:
            response = await agent.run(message)
        
        await ctx.send_message({"parsed": response.text})


class SecondExecutorWithTools(Executor):
    """Second executor - tools won't work due to wrong agent name."""
    
    def __init__(self, chat_client: AzureAIClient):
        self.chat_client = chat_client
        super().__init__(id="second_executor")
    
    @handler
    async def handle(self, payload: dict, ctx: WorkflowContext[dict]) -> None:
        mcp_tool = MCPStreamableHTTPTool(
            name="my-mcp-server",
            url="http://localhost:8000/mcp",
        )
        
        async with mcp_tool:
            async with ChatAgent(
                chat_client=self.chat_client,
                name="second-agent-with-tools",  # IGNORED - uses "first-agent"
                instructions="Use the available tools to look up data.",
            ) as agent:
                # BUG: API request sends 'agent': {'name': 'first-agent'}
                # Model may not recognize tools with wrong agent context
                response = await agent.run(
                    "Look up invoice INV-001",
                    tools=mcp_tool,
                )
        
        await ctx.send_message({"result": response.text})


def build_workflow() -> Workflow:
    """Build workflow with SHARED client (causes bug)."""
    # Single shared client instance
    shared_client = AzureAIClient(
        project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
        model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
        credential=DefaultAzureCredential(),
    )
    
    first = FirstExecutor(chat_client=shared_client)
    second = SecondExecutorWithTools(chat_client=shared_client)
    
    return (
        WorkflowBuilder(name="buggy-workflow", description="Demonstrates agent_name bug")
        .set_start_executor(first)
        .add_edge(first, second)
        .build()
    )


async def main():
    workflow = build_workflow()
    result = await workflow.run(ChatMessage(role="user", text="Test input"))
    print(result)


if __name__ == "__main__":
    asyncio.run(main())

Debug Output Showing the Bug

When running with debug logging enabled:

import logging
logging.basicConfig(level=logging.DEBUG)

First executor request (correct):

DEBUG:openai._base_client:Request options: {
    ...
    'extra_json': {'agent': {'name': 'first-agent', ...}}
}

Second executor request (BUG - wrong agent name):

DEBUG:openai._base_client:Request options: {
    ...
    'extra_json': {'agent': {'name': 'first-agent', ...}}  # Should be 'second-agent-with-tools'!
}

Expected Behavior

Each ChatAgent should use its own name in API requests, regardless of whether the underlying AzureAIClient is shared.

Options:

  1. Always update: Change the condition from if agent_name and not self.agent_name to always update when a new agent name is provided
  2. Per-request agent name: Pass the agent name per-request rather than storing it on the client
  3. Document the limitation: If sharing clients is unsupported, document that each executor needs its own client instance

Workaround

Use a factory function to create separate AzureAIClient instances for each executor:

"""Workaround: Use factory function instead of shared client."""
from typing import Callable

from agent_framework_azure_ai import AzureAIClient
from azure.identity.aio import DefaultAzureCredential


def build_workflow_with_factory() -> Workflow:
    """Build workflow using factory function (WORKAROUND)."""
    
    # Factory creates NEW client for each executor
    def create_client() -> AzureAIClient:
        return AzureAIClient(
            project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
            model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
            credential=DefaultAzureCredential(),
        )
    
    # Each executor gets its own client instance
    first = FirstExecutor(chat_client=create_client())
    second = SecondExecutorWithTools(chat_client=create_client())
    
    return (
        WorkflowBuilder(name="working-workflow", description="Uses factory workaround")
        .set_start_executor(first)
        .add_edge(first, second)
        .build()
    )

Suggested Fix

Modify _update_agent_name_and_description to always update when a new name is provided:

# In agent_framework_azure_ai/_chat_client.py
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None) -> None:
    """Update the agent name in the chat client.

    Args:
        agent_name: The new name for the agent.
        description: The new description for the agent.
    """
    # Always update if a new value is provided (FIX)
    if agent_name:
        self.agent_name = agent_name
    if description:
        self.agent_description = description

Or, pass the agent name per-request in the API call rather than storing it on the client.

Environment

  • Python: 3.12
  • agent-framework-core: 1.0.0b260123
  • agent-framework-azure-ai: 1.0.0b260123
  • Azure AI Foundry: v2 (project_endpoint)
  • Transport: Responses API

Additional Context

This bug was discovered while building a multi-executor workflow for AP vendor email processing. The workflow has 4 executors:

  1. RequestInterpreter - parses email
  2. DataLookupAgent - queries MCP server for vendor/invoice data (tools not called)
  3. ExtractionClassifier - classifies inquiry
  4. ResponseGenerator - drafts response

The DataLookupAgent executor has MCP tools connected and available, but the model never invokes them. Instead, it outputs text like "Calling lookup_invoice_tool for invoice..." without actually making the tool call.

Debug logging revealed that all executors were sending 'agent': {'name': 'request-interpreter'} in their API requests, even though DataLookupAgent specifies name="data-lookup-agent" in its ChatAgent constructor.

The workaround (using a factory function to create separate clients) resolved the issue and tools are now called correctly.

Code Sample

Error Messages / Stack Traces

Package Versions

1.0.0b260123

Python Version

3.12

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions