-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
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):
- Create a
HandoffBuilderworkflow withAzureOpenAIResponsesClientand multiple agents - Configure autonomous mode with
.with_autonomous_mode() - 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
- Bug 1 is specific to
AzureOpenAIResponsesClient— it does not occur withAzureOpenAIChatClientbecause the Chat Completions API uses explicit message history rather thanprevious_response_idcontinuation. - Bug 2 affects both clients, but with
AzureOpenAIChatClientthe symptoms may be less visible because the chat completions API doesn't have the same context model as Responses. - The
AzureOpenAIChatClienthas a separate bug withHandoffBuilder— see companion issue Python: [Bug]: HandoffBuilder with AzureOpenAIChatClient crashes with "Invalid type for messages[N].content: expected string, got object" #4052. - Related closed issues: Azure Foundry v2 (AzureAIClient) - Invalid Payload Error During Workflow Handoffs with HandoffBuilder #3097, Python: HandoffBuilder incompatible with Azure AI Agent Service (tools cannot be added at request time) #3713, Python: [Bug]: Group Chat orchestrator message cleanup issue #3705.