Skip to content

fix(sdk-py): keep with_timeout() from cancelling the shared run-completion future (#9609) - #9625

Closed
AaronG (AaronG18) wants to merge 1 commit into
langchain-ai:mainfrom
AaronG18:fix/9609-output-with-timeout-cancels-shared-future
Closed

AaronG (AaronG18) wants to merge 1 commit into
langchain-ai:mainfrom
AaronG18:fix/9609-output-with-timeout-cancels-shared-future

Conversation

@AaronG18

Copy link
Copy Markdown

Fixes #9609

Problem

thread.output.with_timeout(s) documents that it returns a fresh awaitable and leaves the original thread.output unaffected. It did not: _OutputAwaitable._fetch() passed _wait_for_run_done() straight into asyncio.wait_for, and _wait_for_run_done() awaits the shared _run_done future directly. On timeout, wait_for cancels the task it awaits and that cancellation propagates into _run_done itself. Every later await thread.output then raises CancelledError, and a later run failure is hidden by the cancellation.

Fix

Shield the shared future during the bounded wait:

terminal = await asyncio.wait_for(
    asyncio.shield(self._thread._wait_for_run_done()),
    timeout=self._timeout,
)

A timeout now cancels only the bounded waiter. This follows the existing pattern in the same module (stream.py:1849, await asyncio.wait_for(asyncio.shield(gate), timeout=timeout)) and leaves the "a cancelled task is not respawned" behaviour untouched for callers that wrap thread.output in asyncio.wait_for themselves. Scope: one line of behaviour plus a comment, no public API change.

Regression test

test_output_with_timeout_does_not_cancel_original_output in tests/streaming/test_thread_stream.py:

  1. let with_timeout(0.01) expire while the lifecycle is still in flight,
  2. let the run complete normally afterwards,
  3. assert the original thread.output still resolves to the final state.

Red before the fix (CancelledError at stream.py:1977, return await self._run_done), green after. No network or credentials are needed (httpx ASGITransport FakeServer).

Test results

  • pytest tests/streaming/test_thread_stream.py tests/streaming/test_lifecycle_watcher.py -q -> 80 passed
  • pytest tests/streaming/test_thread_stream.py -q -k with_timeout -> 4 passed (3 existing + 1 new)

Single commit based on current upstream main (e5e37b5d7).

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

This PR has been automatically closed because you are not assigned to the linked issue.

External contributors must be assigned to an issue before opening a PR for it. Please:

  1. Comment on the linked issue to request assignment from a maintainer
  2. Once assigned, your PR will be reopened automatically

Maintainers: reopen this PR or remove the missing-issue-link label to bypass this check.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sdk-py: output.with_timeout() cancels the original thread.output

1 participant