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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions