Skip to content

Python: Hosted LRA draft updates#7122

Draft
alliscode wants to merge 18 commits into
microsoft:mainfrom
alliscode:durable-ha
Draft

Python: Hosted LRA draft updates#7122
alliscode wants to merge 18 commits into
microsoft:mainfrom
alliscode:durable-ha

Conversation

@alliscode

Copy link
Copy Markdown
Member

Motivation & Context

Description & Review Guide

  • What are the major changes?
  • What is the impact of these changes?
  • What do you want reviewers to focus on?

Related Issue

Fixes #

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

alliscode and others added 9 commits July 13, 2026 14:50
- azure-ai-agentserver-core: 2.0.0b5 -> 2.0.0b7 (resilient task primitive, event streaming API)
- azure-ai-agentserver-invocations: 1.0.0b3 -> 1.0.0b6 (bug fixes, requires core >=b7)
- azure-ai-agentserver-responses: 1.0.0b7 -> 1.0.0b8 (resilient background responses, steerable conversations, developer checkpoints)

New versions are unreleased on PyPI; source them via direct URL to the
pinned commit in azure-sdk-for-python via [tool.uv.sources].

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix resilient_background rename: ResponsesServerOptions no longer
  accepts durable_background (renamed to resilient_background in b8).
  The old name caused a silent TypeError that disabled crash recovery
  in hosted environments.

- Add steerable_conversations parameter to ResponsesHostServer.__init__.
  When True, concurrent turns on the same conversation chain are queued
  rather than rejected with 409 conversation_locked. The flag is threaded
  into ResponsesServerOptions and composed with resilient_background in
  the auto-enable path for hosted environments.

- Fix streaming cancellation terminal: both _handle_inner_agent and
  _handle_inner_workflow now emit response.completed before returning
  when cancellation_signal fires. Per spec §10, the handler must emit
  a terminal event; the framework overrides completed → cancelled when
  context.client_cancelled=True (real client cancel), and leaves it as
  completed for steering pressure (client_cancelled=False). Previously
  returning without a terminal caused the framework to force failed,
  which broke the steered-turn chain.

- Add comment in _handle_inner_workflow explaining the relationship
  between previous_response_id and conversation_chain_id for steerable
  turns (they resolve to the same key, so the existing restore logic
  is already correct).

- Update tests: rename resilient_background tests, update cancellation
  test to assert response.completed is emitted, add three new tests
  for steerable_conversations threading and precedence rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix async generator handler signature (b8 compatibility):
  _handle_response was async def returning AsyncIterable. The b8 SDK
  passes the raw handler result directly to _intercept_checkpoints which
  does 'async for raw in handler_iterator' — this fails on a coroutine
  with TypeError (caught as B8 pre-creation error), silently producing
  empty responses. Changed to async generator (using yield) so calling
  it returns an AsyncGenerator immediately iterable without awaiting.

Update 01_basic sample (durable_background -> resilient_background):
  main.py comments and README referenced the old durable_background field
  name (renamed to resilient_background in b8). Updated both to use the
  correct API and added steerable_conversations documentation.

Add steering integration test (test_steering.py):
  Self-contained end-to-end test using Hypercorn on a loopback port and
  concurrent asyncio tasks. Demonstrates the full steering flow:
  turn 1 streams, turn 2 arrives while turn 1 is in_progress and gets
  status=queued, turn 1 emits response.completed (cancellation terminal
  fix), turn 2 runs and completes. Requires clean ~/.agentserver state
  (no stale task files) on first run; uses unique UUID per test run.

Also add pre-existing sample exclusions to pyrightconfig.samples.json
for monty/tools samples not installed in the dev environment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the mock slow agent with a real FoundryChatClient + Agent using
FOUNDRY_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME env vars,
consistent with the 01_basic sample. Loads .env via python-dotenv.
Asks the agent to count to 100 (produces a long streaming response),
then steers with a joke request after 2s.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
resilient_background=True was previously auto-enabled when running in a
hosted Foundry environment. This is now opt-in: callers must pass it
explicitly via ResponsesServerOptions.

  server = ResponsesHostServer(
      agent,
      options=ResponsesServerOptions(resilient_background=True),
  )

Reasoning: auto-enabling crash recovery silently can cause unexpected
behaviour for agents with non-idempotent side effects. Requiring explicit
opt-in makes the behaviour transparent and intentional.

Updates:
- _responses.py: remove auto-enable logic and AgentConfig check
- test_responses.py: replace auto-enable tests with a single test
  asserting resilient_background is never auto-enabled
- 01_basic/main.py: rewrite comment to describe opt-in pattern
- 01_basic/README.md: rewrite durability section as opt-in

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add client.py — a two-terminal steering demonstration.  Run main.py in
one terminal and client.py in a second to see steering in action with a
real Foundry agent:

- Turn 1 streams the agent counting to 50
- 2 s later, turn 2 arrives on the same conversation with a different question
- Turn 2 is accepted with status=queued (not HTTP 409)
- Turn 1's handler is cancelled and emits response.completed with partial output
- Turn 2 runs and its answer is printed

client.py accepts an optional SERVER_URL argument so it can be pointed at
a deployed instance as well as localhost.

Also enable steerable_conversations=True in main.py and update README with
the two-terminal usage instructions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 01_basic: restore to minimal server-only sample; remove client.py,
  test_steering.py, and all steering/resilience content from README.
  Add brief 'Next steps' links to 13 and 14.

- 13_steering: new sample demonstrating steerable_conversations=True.
  main.py starts the server; client.py is an interactive two-terminal
  demo that sends two concurrent turns and shows the queued/steered flow.

- 14_resilience: new sample demonstrating resilient_background=True.
  main.py starts the server with crash recovery enabled. README walks
  through testing crash recovery locally (kill the server mid-response,
  restart, and poll the response to see it recover).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ence

The previous README asked users to Ctrl+C the server at the right moment
while counting to 1000, which was unreliable and the model often refused
the prompt anyway.

demo.py orchestrates everything automatically:
1. Starts main.py as a subprocess
2. Sends a background request (returns in_progress immediately)
3. Kills the server after 2s (no timing pressure)
4. Restarts the server (recovery scanner fires)
5. Polls until the response completes and prints it

Uses a substantive prose prompt (explain how the internet works) that
reliably generates a long response without model refusals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 20:58
@giles17 giles17 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Jul 14, 2026
@github-actions github-actions Bot changed the title Hosted LRA draft updates Python: Hosted LRA draft updates Jul 14, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python “Foundry hosted agents / Responses” hosting experience by adding new resiliency-focused samples and extending the agent-framework-foundry-hosting Responses adapter to better support durable workflow checkpointing and updated SDK behaviors (including MCP output shape changes). It also hardens observability tool-definition serialization (especially for Azure SDK “model” tool objects) and updates tests accordingly.

Changes:

  • Add new hosted Responses samples for steerable conversations, crash recovery, and a deterministic durable-workflow recovery demo (13_steering, 14_resilience, 15_workflow_resilience).
  • Update ResponsesHostServer workflow handling to synchronize workflow checkpoints with durable response snapshots and align output-item behavior with the current SDK (e.g., MCP output emitted as separate items).
  • Improve OTel tool-definition serialization to handle Azure SDK Mapping-based tool models and to skip (with warnings) individual unserializable tools rather than failing telemetry.
Show a summary per file
File Description
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/requirements.txt Adds minimal deps for the deterministic durable-workflow recovery demo.
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/README.md Documents the durable workflow recovery scenario and how to run it.
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/main.py Implements a deterministic 4-stage workflow configured for resilient background execution.
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/Dockerfile Container entrypoint for hosting the workflow sample.
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/demo.py Automates host interruption/replacement and validates durable progress/output.
python/samples/04-hosting/foundry-hosted-agents/responses/15_workflow_resilience/.env.example Optional knobs for state dir/stage delay and telemetry behavior.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/requirements.txt Adds deps for the crash-recovery (resilient_background) sample.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/README.md Explains crash recovery behavior and how the demo works.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/main.py Minimal server enabling resilient_background=True.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/Dockerfile Container entrypoint for the resilience sample.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/demo.py Automates restart-based recovery and polls to completion.
python/samples/04-hosting/foundry-hosted-agents/responses/14_resilience/.env.example Documents required env vars for the Foundry-connected sample.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/requirements.txt Adds deps for the steerable conversations sample.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/README.md Explains queued turns/steering behavior and how to run.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/main.py Minimal server enabling steerable_conversations=True.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/Dockerfile Container entrypoint for the steering sample.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/client.py Client that demonstrates steering by sending overlapping turns.
python/samples/04-hosting/foundry-hosted-agents/responses/13_steering/.env.example Documents required env vars for the Foundry-connected sample.
python/samples/04-hosting/foundry-hosted-agents/responses/01_basic/README.md Adds “Next steps” links to the new steering/resilience samples.
python/pyrightconfig.samples.json Excludes additional sample scripts from pyright sample config.
python/pyproject.toml Updates uv config (includes a new find-links entry).
python/packages/foundry/tests/foundry/test_foundry_chat_client.py Adds regression coverage for OTel tool-definition serialization with code interpreter tools.
python/packages/foundry_hosting/tests/test_responses.py Updates/expands tests for checkpoint synchronization, MCP output shape, and streaming-based behavior.
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py Implements workflow checkpoint synchronization + adjusts response handling/output tracking.
python/packages/core/tests/core/test_observability.py Adds tests ensuring unserializable tool definitions are skipped with warnings.
python/packages/core/agent_framework/observability.py Improves tool-definition serialization (Azure SDK models + partial failure handling).

Review details

  • Files reviewed: 26/27 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread python/pyproject.toml Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 77%

✓ Correctness

The major refactoring of _handle_response introduces a double-emit of response.completed on two code paths: (1) when cancellation_signal fires, the cancellation block emits response.completed at line 847 then breaks, but the post-loop code at line 855 unconditionally emits it again; (2) the consent-error path in _handle_inner_agent yields response.completed at line 932 and then returns, but since this event flows through the caller's async for loop, the post-loop code at line 855 emits a second response.completed. These result in duplicate terminal SSE events on the stream. Two clear correctness issues: (1) a developer-local Windows path hardcoded in pyproject.toml that will break dependency resolution for all other contributors, and (2) a duplicated 'Deploying the Agent to Foundry' section in the 01_basic README. The test refactoring and new test additions appear logically sound. The uv.lock file has been updated to reference three azure-ai-agentserver packages from a local Windows developer path (C:/Users/bentho/src/long-running-agents-private-preview/artifacts/wheels) instead of PyPI. This makes the lock file unresolvable on any other machine, CI, or non-Windows environment. The sample code in 15_workflow_resilience is straightforward and has no correctness issues.

✓ Security Reliability

The main reliability concern is a double response.completed emission on the cancellation path in _handle_response: line 847 emits it inside the cancellation handler, then after break execution falls through to line 855 which emits it again. Additionally, the _drive_workflow_stream finally block has a narrow suppress that won't catch workflow runtime exceptions from already-completed tasks, potentially producing confusing traceback chaining. The most significant issue is a hardcoded developer-local Windows path committed to python/pyproject.toml (find-links), which will cause uv resolution failures or warnings for all other contributors/CI and leaks a developer username. There is also a duplicate 'Deploying the Agent to Foundry' section in the 01_basic README. The uv.lock file has been modified to source three critical azure-ai-agentserver packages from absolute Windows filesystem paths on a developer's local machine (C:/Users/bentho/src/long-running-agents-private-preview/artifacts/wheels). This will break dependency resolution in CI/CD, other developers' machines, and any non-local environment. The PR title indicates this is a draft, but this is a blocking reliability issue that also leaks internal filesystem structure. The sample code (demo.py, main.py) has no security or reliability concerns.

✓ Test Coverage

The PR adds significant new functionality around workflow checkpoint synchronization and durable recovery, with good test coverage for the core happy paths (synchronizer acknowledgement, recovery with persisted checkpoints, cancellation signals). However, there are notable gaps: the shutdown/exit_for_recovery code path (line 829-835) has no test at all, the _WorkflowCheckpointSynchronizer.close() error-handling logic (draining waiters/pending queue) is untested, and the non-workflow agent warning when resilient_background=True (line 656-659) has no assertion verifying the warning is emitted. The _drive_workflow_stream method's exception propagation from a failing workflow (non-StopAsyncIteration) lacks targeted coverage. The PR adds substantial new test coverage for durable workflow checkpoint features (synchronization, recovery, steering, resilience). The new TestDurableResponseStreamSeding tests meaningfully cover the checkpoint synchronization protocol, recovery paths, and cancellation signal behavior. The refactoring of _make_agent to streaming-only is consistent with the production code becoming streaming-only. The uv.lock file references local Windows paths (C:/Users/bentho/src/..) for three azure-ai-agentserver-* packages. This makes dependency installation and the entire test suite non-runable in CI or on any other developer's machine. The new sample (15_workflow_resilience) lacks automated unit tests but its demo.py serves as a manual integration test, which is acceptable for samples.

✗ Failure Modes

The most significant issue is a hardcoded developer-specific Windows path committed in python/pyproject.toml (find-links pointing to C:/Users/bentho/src/..). This will cause dependency resolution failures on CI and for all other developers since the azure-ai-agentserver-* packages are not on PyPI and this path won't exist on other machines. There is also a duplicate 'Deploying the Agent to Foundry' section in the 01_basic README. The uv.lock and pyproject.toml reference absolute local Windows paths (C:/Users/bentho/src/long-running-agents-private-preview/artifacts/wheels) for three critical azure-ai-agentserver-* packages. These paths won't resolve on CI or any other developer's machine, causing dependency installation failures. The PR title says 'draft updates', so this may be intentional for in-progress work, but it should not be merged as-is.

✗ Design Approach

I found one design regression in the Foundry hosted-response adapters: several hosted tool-call items are now converted back into plain function_call content without the transcript-only marker that the rest of the repo uses for provider-owned Responses items. That changes their semantics during replay/round-trip compared with the established hosted Responses pattern. The new crash-recovery sample advertises and enables resilient_background on a plain Agent, but the host implementation explicitly limits that feature to workflow agents. That makes the sample's core design misleading: users who follow it will not get the recovery behavior the README describes.

Flagged Issues

  • Both python/pyproject.toml:63 and python/uv.lock (lines 1317–1352) commit absolute developer-local Windows paths (C:/Users/bentho/src/long-running-agents-private-preview/artifacts/wheels) for azure-ai-agentserver-core, -invocations, and -responses. Since these packages are not on PyPI and the documented install path consumes the lockfile via frozen resolution (python/DEV_SETUP.md:232-239), dependency resolution will fail on CI and all other contributors' machines. Remove the hardcoded find-links from the committed pyproject.toml and regenerate the lockfile against a shared registry or committed wheel source.
  • Hosted Foundry response-item adapters now drop informational_only=True from provider-owned tool-call transcript items, diverging from the established pattern in python/packages/openai/agent_framework_openai/_chat_client.py:2309-2337 and the Content.from_function_call contract in python/packages/core/agent_framework/_types.py:827-828. This changes replay/round-trip semantics so that these provider-owned calls may be incorrectly invoked locally.

Automated review by alliscode's agents

Comment thread python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py Outdated
Comment thread python/uv.lock Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

Both python/pyproject.toml:63 and python/uv.lock (lines 1317–1352) commit absolute developer-local Windows paths (C:/Users/bentho/src/long-running-agents-private-preview/artifacts/wheels) for azure-ai-agentserver-core, -invocations, and -responses. Since these packages are not on PyPI and the documented install path consumes the lockfile via frozen resolution (python/DEV_SETUP.md:232-239), dependency resolution will fail on CI and all other contributors' machines. Remove the hardcoded find-links from the committed pyproject.toml and regenerate the lockfile against a shared registry or committed wheel source.


Source: automated DevFlow PR review

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

Hosted Foundry response-item adapters now drop informational_only=True from provider-owned tool-call transcript items, diverging from the established pattern in python/packages/openai/agent_framework_openai/_chat_client.py:2309-2337 and the Content.from_function_call contract in python/packages/core/agent_framework/_types.py:827-828. This changes replay/round-trip semantics so that these provider-owned calls may be incorrectly invoked locally.


Source: automated DevFlow PR review

TaoChenOSU and others added 8 commits July 14, 2026 14:18
- Switch agentserver wheel sources from GitHub raw URLs to find-links at
  local long-running-agents-private-preview checkout; remove [tool.uv.sources]
  URL entries from python/pyproject.toml
- Re-lock uv.lock for find-links path sources
- Guard tracker.close() in cancellation path to avoid ValueError when
  the SDK state machine is in not_started state
- Fix test assertions to access _ContextAwareCheckpointStorage._inner.storage_path
  instead of .storage_path (which is not exposed directly on the wrapper)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1674d491-af7d-4f9e-99aa-5a829db81591
# checkpoints and approval requests to survive a process restart.
# Otherwise local development keeps everything in memory.
use_file_storage = self.config.is_hosted or (resilient_background and self._is_workflow_agent)
if self._is_workflow_agent and use_file_storage:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self._is_workflow_agent and use_file_storage:
if use_file_storage:

We don't need to check self._is_workflow_agent since it's been checked above

Comment thread python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py Outdated
Comment on lines +806 to +811
if not context.is_recovery and workflow_checkpoint_execution.restore_checkpoint_id is not None:
# ``response.created`` strips framework-internal metadata
# from its wire payload. Explicitly checkpoint the live
# response before executing the new turn so a crash before
# its first superstep can restore the prior durable state.
yield cast(ResponseStreamEvent, response_event_stream.checkpoint())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: not sure why this is necessary?

Comment thread python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py Outdated
)

async for item in _to_outputs_for_messages(
async for event in self._drive_workflow_stream(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is restoring the workflow to a checkpointed state. We don't stream out events during restoration.

if checkpoint_execution.restore_checkpoint_id is None or checkpoint_execution.restore_storage is None:
raise RuntimeError("Current-turn recovery requires an exact workflow checkpoint reference.")

async for workflow_event in self._agent.workflow.run(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we are invoking the workflow directly?

checkpoint_id=checkpoint_execution.restore_checkpoint_id,
checkpoint_storage=checkpoint_execution.restore_storage,
):
for update in self._agent._convert_workflow_event_to_agent_response_updates( # pyright: ignore[reportPrivateUsage]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we invoke the workflow as an agent, we don't need to parse the workflow events.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1674d491-af7d-4f9e-99aa-5a829db81591
provider.source_id,
)

resilient_background = bool(options and options.resilient_background)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pin the first azure-ai-agentserver-responses build that actually exposes these recovery APIs? The final lock resolves the public 1.0.0b8 artifact, but that build has no ResponsesServerOptions.resilient_background/steerable_conversations, ResponseContext.shutdown/recovery members, or ResponseEventStream.checkpoint; the new samples fail at construction and ordinary/workflow requests raise AttributeError. This is also what the current package and sample CI failures are reporting.

reference = checkpoint_task.result()
if reference is None:
break
yield cast(ResponseStreamEvent, publisher.checkpoint_event(reference))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we close the active output builder before snapshotting this workflow checkpoint? The agentserver SDK does not add an item to response.output until response.output_item.done, but workflow checkpoints are saved after their output events have been consumed. A crash after this checkpoint can therefore resume past an update that is absent from the persisted response, permanently dropping that text or tool call.

self._agent.workflow.name,
latest_checkpoint.checkpoint_id,
)
elif context.is_recovery and request.previous_response_id is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this fallback cover context.conversation_id too? response.created is emitted before the baseline checkpoint event, so a process exit in that window recovers with no structured checkpoint reference. For a conversation-scoped request this condition skips the existing conversation checkpoint and reruns the input on a fresh workflow, losing the conversation's durable state.

contents.append(Content.from_text_reasoning(id=reasoning.id, text=summary.text))
else:
contents.append(Content.from_text_reasoning(id=reasoning.id))
contents.append(Content.from_text(summary.text))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep reconstructing this as Content.from_text_reasoning(id=reasoning.id, text=summary.text)? Turning it into ordinary text removes the marker that _mcp_call_ids_paired_with_reasoning() uses to keep reasoning and its hosted MCP call together.

On stateless multi-turn replay, the MCP call is then sent without its required reasoning item and OpenAI rejects the request with HTTP 400, reintroducing #6883/#6074.

mcp_call = stream.add_output_item_mcp_call(
server_label=content.server_name or "default",
name=content.tool_name or "",
item_id=content.call_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we preserve one MCP call ID across the persisted call and output? add_output_item_mcp_call() now generates a new item ID, while aoutput_item_custom_tool_call_output() persists the original Content.call_id; on the next turn the OpenAI serializer only coalesces results whose call_id equals the persisted mcp_call.id, so it drops this result as orphaned. The new round-trip test currently accepts the mismatch and never exercises the real serializer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants