mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-14 04:05:27 +02:00
### Problem `Pregel.astream_events` declared `interrupt_before`, `interrupt_after` and `control` as keyword-only parameters, but forwarded them only on the `version="v3"` branch. On async `version="v1"`/`"v2"` the values were bound to named parameters and never entered `**kwargs`, so they were silently dropped on the way to `astream` — no error, no effect: - `request_drain()` on a caller-supplied `RunControl` did nothing; the graph created its own control and ran to completion. - Static interrupts passed as `astream_events(..., interrupt_before=[...])` never fired. This is a regression introduced by #7677 (first released in `1.2.0a3`). Before that, `Pregel` did not override `astream_events`, and these keywords reached `astream` through `**kwargs` — langchain-core's v1 and v2 event implementations both forward caller kwargs to `astream` (`tracers/log_stream.py`, `tracers/event_stream.py`). Downstream impact: langgraph-api passes run-level `interrupt_before` / `interrupt_after` into `graph.astream_events(..., version="v2", **kwargs)` for runs with `stream_mode="events"` (`models/run.py`, `stream.py`), so server runs using that stream mode have silently dropped static interrupts since langgraph 1.2.0a3. Sync `stream_events(version="v1"/"v2")` is unaffected: langchain-core raises `NotImplementedError` there, so there was no working sync path to break. ### Fix Remove the three parameters from the public runtime implementations of `stream_events` / `astream_events` (and from the explicit passing in their v3 dispatch calls), so the values travel through `**kwargs` again: - **v1/v2** recover the pre-#7677 passthrough. `astream` receives exactly what the caller passed — a supplied value, an explicit `None`, or nothing for an omitted argument. - **v3** is unchanged. The values bind on `_pregel_stream_v3` / `_apregel_stream_v3`, which keep their named parameters and since-inception (#7519, `1.2.0a1`) explicit-`None` defaults: an omitted argument arrives at `astream` as `None`, exactly as before. What `astream` receives after this change: | caller supplies | async v1/v2 | v3 (sync and async) | |---|---|---| | nothing | argument absent | explicit `None` | | explicit `None` | `None` | `None` | | a value | the value | the value | The typed `version="v3"` `@overload` stubs are untouched, so v3 call sites keep their static types (invalid values still fail `ty` there, same as on `main`). `RemoteGraph` and the v3-only `transformers` parameter are untouched. Complexity: net −24 lines in `main.py` with no branches, sentinels or wrappers added. The public capture-and-drop pattern that caused the bug is gone; the two private helpers each have a single call path and forward unconditionally, so they cannot drop anything. Docstrings now describe the three parameters as honored on every async version (type-checked only on the v3 overloads), and no longer promise synchronous v1/v2 event streaming, which langchain-core does not implement. ### Compatibility notes - The three parameters were keyword-only, so no caller can break; keyword callers bind through `**kwargs` identically. - The names leave the runtime signatures (`inspect.signature`), while the v3 overloads keep the typing. - v3 is unchanged: `_pregel_stream_v3`/`_apregel_stream_v3` still default all three to `None` and pass them explicitly, so an override's non-`None` default never applied on v3 and still doesn't. The only behavior change beyond restoring the pre-#7677 passthrough is on v1/v2 with an explicit `None`: since 1.2.0a3 the named-parameter capture dropped it, so an override's non-`None` default was silently applied instead; now the explicit `None` reaches `astream`, as it did before 1.2.0 and as it does on v3. ### Testing New tests in `tests/test_stream_events_v3_kwarg_forwarding.py`: - `interrupt_before` / `interrupt_after` / pre-drained `control` parametrized over v1/v2/v3 (12 of them fail on unpatched `main`). - Sync v3 static interrupts. - Mid-run drain: a node calls `request_drain()`; asserts `GraphDrained` propagates, the caller's own `RunControl` was used, and the checkpoint keeps the pending step. - Recording tests pin, for all three names and on both the async and sync v3 paths, exactly what `(a)stream` receives: supplied values (with `control` identity), explicit `None`, and absence for omitted arguments. Verified by mutation: replacing the implementation with each rejected alternative (unpatched parent; injecting `None` for omitted kwargs on v1/v2; dropping explicit `None` on v1/v2; kwargs-only private helpers) makes the corresponding tests fail. Suites: 246 passed on the four adjacent streaming test files; 906 passed / 1 skipped across every test file calling `(a)stream_events` plus `test_runtime.py`. `ruff check` / `format --check` clean; `ty check langgraph` clean. Not verifiable locally (CI please): Python 3.10 (the new tests carry no version skip), minimum supported langchain-core, and a live langgraph-api server run. ### Release note > Fixed a regression since 1.2.0a3: `Pregel.astream_events` silently dropped > `interrupt_before`, `interrupt_after` and `control` for > `version="v1"`/`"v2"`. Static interrupts and `RunControl` drains now work on > every async version. Server runs using `stream_mode="events"` were affected > and get their static interrupts back.