-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Description
When running a HandoffBuilder workflow, the orchestrator forces store=False on cloned agents (in _clone_chat_agent(), line 375 of _handoff.py). This is intentional and well-documented in the code comments:
"Handoff workflows already manage full conversation context explicitly across executors. Keep provider-side conversation storage disabled to avoid stale tool-call state (Responses API previous_response chains)."
However, when the handoff replays conversation history to the next agent, the serialized input items (specifically reasoning and function_call) retain their server-assigned IDs (rs_* and fc_* prefixes). Since store=False means the API did not persist those items, the IDs reference non-existent objects. The Responses API then rejects the request with:
Item with id 'rs_...' not found. Items are not persisted when store is set to false.
Root cause
In _responses_client.py, the method _prepare_content_for_openai() unconditionally includes server-assigned IDs:
- Reasoning items (line ~975):
if content.id: ret["id"] = content.id— always includes thers_*ID - Function call items (line ~1051):
"id": fc_id— always includes thefc_*ID
When these items are replayed as part of a handoff conversation under store=False, the API cannot resolve those IDs because nothing was persisted.
Steps to reproduce
- Run the official sample
handoff_autonomous.pyusingAzureOpenAIResponsesClient(orAzureAIClient) - Wait for the first handoff (e.g., coordinator → research_agent)
- When the second agent responds and the conversation is replayed back with
reasoningorfunction_callitems from the previous turn, the API rejects it
This is reproducible with any model and with both AzureOpenAIResponsesClient and AzureAIClient.
What did you expect to happen?
The handoff should successfully replay conversation history to the next agent without API errors, even under store=False.
What happened?
The API rejects the input because it references item IDs (rs_*, fc_*) that were never persisted (due to store=False).
Code Sample
The exact official sample that reproduces the bug:
# https://github.com/microsoft/agent-framework/blob/main/python/samples/03-workflows/orchestrations/handoff_autonomous.py
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
from dotenv import load_dotenv
load_dotenv()
def create_agents(client):
coordinator = client.as_agent(
instructions="You are a coordinator. You break down a user query into a research task and a summary task. Assign the two tasks to the appropriate specialists, one after the other.",
name="coordinator",
)
research_agent = client.as_agent(
instructions="You are a research specialist that explores topics thoroughly. When given a research task, break it down into multiple aspects and explore each one. Continue your research across multiple responses. When you have covered the topic comprehensively, return control to the coordinator.",
name="research_agent",
)
summary_agent = client.as_agent(
instructions="You summarize research findings. Provide a concise, well-organized summary. When done, return control to the coordinator.",
name="summary_agent",
)
return coordinator, research_agent, summary_agent
async def main():
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
coordinator, research_agent, summary_agent = create_agents(client)
workflow = (
HandoffBuilder(
name="autonomous_iteration_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") >= 5
),
)
.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): 5,
resolve_agent_id(research_agent): 10,
resolve_agent_id(summary_agent): 5,
}
)
.build()
)
request = "Perform a comprehensive research on Microsoft Agent Framework."
async for event in workflow.run(request, 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)
if __name__ == "__main__":
asyncio.run(main())Error Messages / Stack Traces
openai.BadRequestError: Error code: 400 - {
'error': {
'message': "Item with id 'rs_...' not found. Items are not persisted when store is set to false.",
'type': 'invalid_request_error',
'param': 'input',
'code': None
}
}
The error is raised during the second agent invocation after a handoff, when the conversation replay includes reasoning or function_call items with server-assigned IDs from the prior agent's response.
Package Versions
agent-framework: 1.0.0rc2agent-framework-orchestrations: 1.0.0b260225agent-framework-azure-ai: 1.0.0rc2
Python Version
Python 3.13
Additional Context
Relationship to #4053
This bug is distinct from the issues tracked in #4053 (stale previous_response_id and context loss). The fixes merged via PR #3911 (clearing service_session_id and using _full_conversation for cache) are already present in our installed version (1.0.0b260225) and correctly address those problems. However, those fixes do not address the server-assigned ID leakage described here.
Proposed fix
In _prepare_content_for_openai() (in _responses_client.py), omit the id field from reasoning and function_call items when store=False. The items should be replayed by value rather than by reference, since there is nothing persisted server-side to reference.
Possible implementation (in _prepare_options or _prepare_messages_for_openai):
# After building the input items, strip server-assigned IDs when store=False
if run_options.get("store") is False and "input" in run_options:
for item in run_options["input"]:
if isinstance(item, dict) and item.get("type") in ("reasoning", "function_call"):
item.pop("id", None)Alternatively, the check could be done at the serialization level in _prepare_content_for_openai() if store is available in scope:
- For
text_reasoning: only includeret["id"] = content.idwhenstoreis notFalse - For
function_call: generate a freshfc_prefixed ID (e.g., fromcall_id) instead of reusing the server-assigned one, or omit it
Note: We have validated this approach as a monkeypatch in our production code and it resolves the issue without observable side effects. However, the team should evaluate whether omitting these IDs could affect other scenarios (e.g., when store=True and IDs are expected).
Working monkeypatch (temporary workaround)
For anyone encountering this issue, the following monkeypatch can be applied before running the workflow:
import agent_framework.openai._responses_client as _rc
_orig_prepare = _rc.RawOpenAIResponsesClient._prepare_options
async def _patched_prepare(self, messages, options, **kwargs):
run_options = await _orig_prepare(self, messages, options, **kwargs)
if run_options.get("store") is False and "input" in run_options:
for item in run_options["input"]:
if isinstance(item, dict) and item.get("type") in ("reasoning", "function_call"):
item.pop("id", None)
return run_options
_rc.RawOpenAIResponsesClient._prepare_options = _patched_prepareMetadata
Metadata
Assignees
Labels
Type
Projects
Status