microsoft / microsoft/agent-framework

Python: [Bug]: Background Responses tool loop fails when chaining tool output to a completed background response

Open
#7,538 4 comments 0 reactions 1 assignee View on GitHub

@eavanvalkenburg is already working on this.

Since Aug 26, 2026.

agents python
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

Summary

OpenAIChatClient local tool invocation fails when the initial Responses request runs with background=True and store=True.

After the completed background response emits a valid function_call, Agent Framework executes the local tool and submits its function_call_output using previous_response_id. Azure rejects that follow-up with:

No tool call found for function call output with call_id ...

Retrieving the completed predecessor confirms that it contains the exact same call_id. The output is therefore not orphaned in the client payload.

The equivalent stored foreground chain succeeds. The same background-only failure also reproduces with the raw OpenAI Python SDK, so the underlying service defect is below Agent Framework and is tracked in Azure/azure-sdk-for-python#46092. I am filing this here because Agent Framework's background tool loop currently selects the affected continuation strategy and provides no supported fallback or early diagnostic.

Expected behavior

One of the following:

  1. A local tool loop started with background=True completes successfully.
  2. Agent Framework continues after a background predecessor by replaying complete local history into a fresh stored background response without previous_response_id.
  3. If background responses with local tools are not supported, Agent Framework rejects the combination before submitting the costly background job and documents the limitation and workaround.

store=False is not a workaround because the Responses API requires storage for background requests.

Actual behavior

The initial background response reaches completed and contains a valid local function_call. The first tool-result follow-up fails with HTTP 400 before the model can consume the result.

Steps to reproduce
  1. Configure the three environment variables used by the sample:
    • AZURE_OPENAI_BASE_URL: an Azure OpenAI-compatible v1 base URL ending in /openai/v1
    • AZURE_OPENAI_SCOPE: the Entra scope used by that endpoint
    • AZURE_OPENAI_MODEL: a deployed reasoning model, tested with gpt-5.4-nano
  2. Run the code sample.
  3. Wait for the initial background response to complete and emit lookup_probe.
  4. Observe the 400 when Agent Framework submits the local tool result.
  5. Change the initial OpenAIChatOptions(background=True) to background=False. The same local tool loop completes.
Repeated result

Using an equivalent raw Responses transport harness:

Variant Result
Production-like structured background request without tools 1/1 completed (27,347 input tokens)
Stored foreground predecessor + previous_response_id 6/6 completed
Stored background predecessor + previous_response_id 0/6 completed; 6/6 returned the exact 400
Fresh stored background response with output/tool-output replay and no previous_response_id 5/6 completed; one unrelated polling timeout, no pairing error

For all six background-chain failures, retrieving the predecessor returned a function_call whose call_id exactly matched both the submitted function_call_output and the call ID named in the error.

This makes the failure deterministic for the tested background continuation path, rather than a missing local tool result or a parallel-tool race.

Code Sample
import asyncio
import os

from agent_framework import AgentSession, tool
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider


@tool
def lookup_probe(step: int) -> str:
    return f"probe-result-step-{step}"


async def main() -> None:
    credential = DefaultAzureCredential()
    client = OpenAIChatClient(
        credential=get_bearer_token_provider(
            credential,
            os.environ["AZURE_OPENAI_SCOPE"],
        ),
        base_url=os.environ["AZURE_OPENAI_BASE_URL"],
        model=os.environ["AZURE_OPENAI_MODEL"],
    )
    agent = client.as_agent(
        name="background_tool_probe",
        instructions="Call lookup_probe with step=1 exactly once, then return its result.",
        tools=[lookup_probe],
        default_options=OpenAIChatOptions(
            store=True,
            reasoning={"effort": "medium", "summary": "auto"},
            allow_multiple_tool_calls=False,
        ),
    )
    session = AgentSession()

    try:
        response = await agent.run(
            "Run the probe.",
            session=session,
            options=OpenAIChatOptions(background=True),
        )
        while response.continuation_token is not None:
            await asyncio.sleep(1)
            response = await agent.run(
                session=session,
                options=OpenAIChatOptions(
                    continuation_token=response.continuation_token,
                ),
            )
        print(response.text)
    finally:
        await client.client.close()
        await credential.close()


if __name__ == "__main__":
    asyncio.run(main())
Error Messages / Stack Traces
openai.BadRequestError: Error code: 400 - {
  'error': {
    'message': 'No tool call found for function call output with call_id call_<redacted>.',
    'type': 'invalid_request_error',
    'param': 'input',
    'code': None
  }
}

The exception is surfaced by Agent Framework as a ChatClientException from the
OpenAIChatClient tool-invocation loop.

For every failed run:

emitted function_call call_id
  == submitted function_call_output call_id
  == call_id returned by responses.retrieve(previous_response_id)
  == call_id named in the 400 response
Package Versions

agent-framework-core: 1.13.0; agent-framework-openai: 1.12.0; openai: 2.48.0; azure-identity: 1.25.3; pydantic: 2.13.4

Python Version

Python 3.14.6

Additional Context
Environment
  • Windows 11
  • Azure OpenAI Responses-compatible v1 endpoint reached through APIM
  • Authentication: DefaultAzureCredential / Entra bearer token, no API key
  • Model tested: gpt-5.4-nano
  • Initial and replayed background requests require store=True
  • parallel_tool_calls=False; the reproduction uses one deterministic local function
Why this is different from related Agent Framework reports
  • microsoft/agent-framework#3304 was a framework bug involving stale conversation IDs and was fixed by #3312.
  • microsoft/agent-framework#5041 reached a raw service reproduction and was closed on 2026-06-09 as fixed upstream. This report reproduces a narrower background-predecessor case on newer packages on 2026-08-06.
  • microsoft/agent-framework#5546 concerns persisted hosted MCP history producing an orphan output. Here the predecessor is completed, uses a plain local function, and retrieval proves that the matching function_call exists.
  • Azure/azure-sdk-for-python#46092 remains open and tracks the underlying Responses chaining defect.
Framework-specific request

I understand that the 400 originates below Agent Framework. The framework-specific problem is that OpenAIChatClient advertises background responses and local tool invocation together, then uses previous_response_id for the tool-result continuation after a completed background response.

Could the Python client do one of the following?

  1. Use full local-history replay without previous_response_id after a background predecessor. The replay must retain the original user input, every response output item including reasoning items, and each function output.
  2. Expose a continuation strategy hook so applications can choose inline replay for this case.
  3. Detect this unsupported combination and fail before submission with an actionable message linking the upstream issue.
  4. Add an integration test that contrasts foreground and background predecessors using the same local function call.

I can provide sanitized per-run response IDs, call-ID evidence, and the full raw transport harness if useful.

Related issues

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.