Skip to content

Python: [Bug]: HandoffBuilder with AzureOpenAIResponsesClient fails with stale previous_response_id and loses conversation context on handoff #4053

@frdeange

Description

@frdeange

Description

When using HandoffBuilder with AzureOpenAIResponsesClient in autonomous mode, the workflow fails after the first handoff cycle. There are two distinct bugs:

Bug 1: Stale previous_response_id causes "No tool output found for function call"

What happened:
After the coordinator invokes a handoff tool (function_call) and hands off to a specialist, the coordinator's response ID (resp_XXX) is stored in session.service_session_id. When the specialist hands back and the coordinator runs again, this stale response ID is sent as previous_response_id to the Responses API. Since the original response contained a pending function_call (the handoff tool) whose output was cleaned from the conversation by clean_conversation_for_handoff(), the API rejects the request.

What I expected:
The handoff workflow should complete without API errors.

Bug 2: Conversation context lost after handoff — agents receive empty/partial history

What happened:
After fixing Bug 1, agents on their 2nd+ invocation receive only partial conversation history. The _cache in HandoffAgentExecutor._run_agent_and_emit() only contains recently broadcast messages, not the full conversation. Since the Responses API no longer carries context via previous_response_id (cleared to fix Bug 1), the agent runs with incomplete context and produces off-topic responses (e.g., researching "French Revolution" instead of the requested topic).

What I expected:
Each agent should see the complete conversation history when it runs, regardless of how many handoffs have occurred.

Steps to reproduce (both bugs):

  1. Create a HandoffBuilder workflow with AzureOpenAIResponsesClient and multiple agents
  2. Configure autonomous mode with .with_autonomous_mode()
  3. Run the workflow — Bug 1 crashes on first handoff back to coordinator; fixing Bug 1 reveals Bug 2

Code Sample

import asyncio
import os
from typing import cast

from agent_framework import Agent, AgentResponseUpdate, Message, resolve_agent_id
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential

async def main():
    client = AzureOpenAIResponsesClient(
        project_endpoint=os.getenv("AZURE_AI_PROJECT_ENDPOINT"),
        deployment_name=os.getenv("AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"),
        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 briefly, 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") >= 2
            ),
        )
        .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): 2,
                resolve_agent_id(research_agent): 2,
                resolve_agent_id(summary_agent): 2,
            }
        )
        .build()
    )

    async for event in workflow.run("Research Microsoft Agent Framework.", stream=True):
        if event.type == "handoff_sent":
            print(f"\nHandoff: {event.data.source} -> {event.data.target}\n")
        elif event.type == "output":
            data = event.data
            if isinstance(data, AgentResponseUpdate) and data.text:
                print(data.text, end="", flush=True)
            elif isinstance(data, list):
                print("\n\nFinal Transcript:")
                for msg in cast(list[Message], data):
                    print(f"{msg.author_name or msg.role}: {msg.text}\n")

asyncio.run(main())

Error Messages / Stack Traces

Bug 1 error (before fix):

openai.BadRequestError: Error code: 400 - {'error': {'message': 'No tool output found for function call call_qgTofhhnqcvlozSsO1OgsgPK.', 'type': 'invalid_request_error', 'param': 'input', 'code': None}}

Full traceback:

File ".../agent_framework/openai/_responses_client.py", line 317, in _stream
    async for chunk in await client.responses.create(stream=True, **run_options):
File ".../openai/_base_client.py", line 1669, in request
    raise self._make_status_error_from_response(err.response) from None
openai.BadRequestError: Error code: 400 - {'error': {'message': 'No tool output found for function call call_qgTofhhnqcvlozSsO1OgsgPK.', ...}}

Bug 2 symptom (after fixing Bug 1):

No error — but agents produce completely off-topic content because they don't see the original user query or prior conversation turns. Only the system message reaches the agent.

Root Cause Analysis

Bug 1: Stale previous_response_id

In _handoff.py, when _is_handoff_requested() detects a handoff, the agent's session.service_session_id still holds the response ID from the coordinator's last response (which contained a pending handoff function_call). On the next invocation, Agent._prepare_run_context() (line 1063 of _agents.py) reads session.service_session_id and passes it as conversation_id, which _responses_client.py translates to previous_response_id. The Responses API rejects this because the referenced response has an unresolved function_call.

Meanwhile, clean_conversation_for_handoff() strips function_call content from the conversation messages — so the handoff tool call is removed from messages but still lives in the server-side response referenced by previous_response_id.

Bug 2: Lost conversation context

In HandoffAgentExecutor._run_agent_and_emit(), the agent runs with self._cache which only contains messages received since the last _cache.clear(). After a handoff, the cache only has the broadcast messages from the other agent (the cleaned response), not the full conversation history including the original user query and prior turns. The _full_conversation list has everything, but it's not used as input to the agent — only _cache is passed to agent.run().

With the Chat Completions API, this was partially masked because the full conversation was usually rebuilt from messages. With the Responses API, once previous_response_id is cleared (to fix Bug 1), there's no implicit context carry-over, making the context loss visible.

Suggested Fixes

Bug 1 fix — Clear session.service_session_id on handoff

In _handoff.py, after detecting a handoff, clear the session to prevent stale previous_response_id:

# In HandoffAgentExecutor._run_agent_and_emit(), after _is_handoff_requested() returns True:
if handoff_target := self._is_handoff_requested(response):
    # ... validation ...

    # Clear the session's service_session_id to prevent stale previous_response_id
    if self._session and self._session.service_session_id:
        self._session.service_session_id = None

    await cast(WorkflowContext[AgentExecutorRequest], ctx).send_message(
        AgentExecutorRequest(messages=[], should_respond=True), target_id=handoff_target
    )
    # ... rest of handoff logic ...

Bug 2 fix — Use full conversation for agent runs

In _handoff.py, replace _cache with the full conversation before running the agent:

# In HandoffAgentExecutor._run_agent_and_emit(), after extending _full_conversation:
self._full_conversation.extend(self._cache)

# Use full conversation history instead of partial cache
self._cache = list(self._full_conversation)

Both fixes have been tested locally and the workflow completes successfully with correct context in all agent invocations.

Package Versions

agent-framework: 1.0.0b260212, agent-framework-orchestrations: 1.0.0b260212

Python Version

Python 3.13.9

Additional Context

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions