Files
langgraph/libs/sdk-py/langgraph_sdk/stream/errors.py
T
Elior Nataf Lackritz e901fc1746 fix(sdk-py): restart the lifecycle watcher when run.start creates the thread
An explicit thread_id can name a thread that run.start is about to create.
Under custom auth its lifecycle stream gets a 404 first, which stopped the
watcher for good, so thread.output raised that 404 after a successful run.
run.start now starts a fresh watcher and run result when the watcher has
stopped, and a 404 on a stream opened before run.start gets one more try
once a run has started.

Also reports the last error frame when lifecycle reconnects run out, and
checks the stream error types with isinstance instead of getattr.
2026-10-16 13:37:35 -04:00

47 lines
1.6 KiB
Python

"""Thread-stream failures, shared by the async and sync clients."""
from __future__ import annotations
from typing import Any
import httpx
from websockets.exceptions import ConnectionClosed, InvalidStatus
def protocol_error(response: dict[str, Any]) -> RuntimeError:
"""Build the error for a protocol `{"type": "error"}` envelope."""
code = response.get("error", "unknown")
message = response.get("message", "")
return RuntimeError(f"Protocol error [{code}]: {message}")
def stream_error_frame(event: Any) -> RuntimeError | None:
"""Return the error in an unsolicited `{"type": "error", "id": null}` frame.
The server sends one just before it closes a thread stream whose read failed.
"""
if (
isinstance(event, dict)
and event.get("type") == "error"
and event.get("id") is None
):
return protocol_error(event)
return None
def is_non_retryable_stream_error(error: BaseException | None) -> bool:
"""Whether reopening the stream would fail the same way.
True for a 4xx other than 408 and 429: the status of a refused stream request
or WebSocket upgrade, or a WebSocket close code of 4000 plus that status. A
server that rejects a WebSocket after accepting it closes with such a code,
because a browser can't read the status of a refused upgrade.
"""
if isinstance(error, (httpx.HTTPStatusError, InvalidStatus)):
status = error.response.status_code
elif isinstance(error, ConnectionClosed) and error.rcvd is not None:
status = error.rcvd.code - 4000
else:
return False
return 400 <= status < 500 and status not in (408, 429)