Skip to content

Python: [Bug]: Handoff workflow with store=False sends server-assigned item IDs (rs_*, fc_*) in input, causing "Item not found" API errors #4357

@frdeange

Description

@frdeange

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 the rs_* ID
  • Function call items (line ~1051): "id": fc_id — always includes the fc_* 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

  1. Run the official sample handoff_autonomous.py using AzureOpenAIResponsesClient (or AzureAIClient)
  2. Wait for the first handoff (e.g., coordinator → research_agent)
  3. When the second agent responds and the conversation is replayed back with reasoning or function_call items 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.0rc2
  • agent-framework-orchestrations: 1.0.0b260225
  • agent-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 include ret["id"] = content.id when store is not False
  • For function_call: generate a fresh fc_ prefixed ID (e.g., from call_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_prepare

Metadata

Metadata

Labels

pythonv1.0Features being tracked for the version 1.0 GA

Type

Projects

Status

Planned

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions