langchain-ai / langchain-ai/langgraph

config typing warning recommends 'RunnableConfig | None', but that exact annotation triggers the warning (under PEP 563) and silently disables config injection

Open
#8,941 2 comments 0 reactions 0 assignees View on GitHub
external
Dominant language
Python
Stars
41.8k
Forks
7.1k
Avg merge
23h 7m
Merged PRs (30d)
30

Description

### Checked other resources

- [x] This is a bug, not a usage question.
- [x] I added a clear and descriptive title that summarizes this issue.
- [x] I used the GitHub search to find a similar question and didn't find it.
- [x] I am sure that this is a bug in LangGraph rather than my code.
- [x] The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).
- [x] This is not related to the langchain-community package.
- [x] I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

### Related Issues / PRs

* #5787 — added this warning
* #5798 — implementation PR (interesting detail: the agent's plan text used the message `'RunnableConfig' or 'Optional[RunnableConfig]'`, which matches the accept-list; the merged code says `'RunnableConfig | None'`, which does not)

### Reproduction Steps / Example Code (Python)

```python
from __future__ import annotations # PEP 563 — annotations become raw strings

import warnings

from typing_extensions import TypedDict

from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, START, StateGraph

class State(TypedDict):
seen: list

def node(state: State, config: RunnableConfig | None = None) -> dict:
# This is the spelling the warning message itself recommends.
return {"seen": [config]}

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
builder = StateGraph(State)
builder.add_node("node", node)
builder.add_edge(START, "node")
builder.add_edge("node", END)
graph = builder.compile()
for w in caught:
print("WARNING:", w.message)

result = graph.invoke({"seen": []}, config={"tags": ["probe"]})
print("injected config:", result["seen"][0])
print("probe tag reached node:", result["seen"][0] is not None and "probe" in result["seen"][0].get("tags", []))
```

### Error Message and Stack Trace (if applicable)

```shell
$ python repro.py
WARNING: The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not 'RunnableConfig | None'.
injected config: None
probe tag reached node: False
```

No exception is raised — the failure is silent: the node receives `config=None` on every invoke, so the callbacks/tracing chain never reaches anything the node calls.

### Description

I'm typing a node's injected `config` parameter as `RunnableConfig | None = None` (in a module using `from __future__ import annotations`, a widely used modern default).

I expect: per the warning message's own recommendation, this spelling is accepted — no warning, and `config` is injected.

What happens instead: the warning fires with a self-referential message (`... should be typed as 'RunnableConfig' or 'RunnableConfig | None', not 'RunnableConfig | None'`), **and config injection is silently disabled** — the node runs with `config=None` on every invoke. We hit this in production: all LLM calls made inside such a node were invisible to Langfuse, because `config` never carried the callback manager into them.

**Root cause** (`langgraph/_internal/_runnable.py`, `RunnableCallable.__init__`): the check compares the RAW `inspect.signature` annotation against the `KWARGS_CONFIG_KEYS` accept entries `(RunnableConfig, "RunnableConfig", Optional[RunnableConfig], "Optional[RunnableConfig]", empty)`. Under PEP 563 the annotation is the string `"RunnableConfig | None"`, which is not in the list, so the branch warns and `continue`s — skipping the `func_accepts["config"]` registration. There is no stringified union entry, and the message recommends exactly the spelling the list rejects.

Verified matrix (langgraph 1.2.11, Python 3.13.12):

| annotation on `config` | `from __future__ import annotations` | no future import (real objects) |
|---|---|---|
| `RunnableConfig \| None` | warns + injection disabled | accepted (only via PEP 604 equality with the `Optional[RunnableConfig]` entry) |
| `None \| RunnableConfig` | warns + injection disabled | accepted |
| `RunnableConfig\|None` (no spaces) | warns + injection disabled | — |
| aliased import `RC \| None` | warns + injection disabled | — |
| bare `RunnableConfig` | accepted | accepted |
| `Optional[RunnableConfig]` | accepted | accepted |
| no annotation | accepted (injection happens) | accepted |

Additional notes:

* The `KWARGS_CONFIG_KEYS` docstring on `main` already acknowledges this class of gap: "This is fully internal and should be further refactored to use `get_type_hints` to resolve forward references and optional types formatted like BaseStore | None." The `store` entries have the same gap.
* The test added with the warning (`tests/test_deprecation.py::test_config_parameter_incorrect_typing`) doesn't cover the future-import + `X | None` case, which is how this shipped.
* Ecosystem projects already suppress this exact warning message in their test suites (e.g. deepeval's langgraph integration tests use `filterwarnings` on it), which masks the silent injection loss.
* Checked today: latest stable (1.2.11) and `main` both still have the unchanged accept-list.

**Suggested fix:** resolve annotations via `typing.get_type_hints(func)` before matching (falling back to the raw annotation if resolution fails, e.g. unresolvable forward refs), or minimally add the stringified `"RunnableConfig | None"` / `"BaseStore | None"` forms to the accept entries and align the warning message with whatever the list actually accepts. Separately worth considering: silently disabling injection is a functional change (callbacks/tracing/configurable all dropped) — arguably it deserves a louder failure mode than a `UserWarning` that recommends a spelling that doesn't work.

### System Info

```shell

System Information
------------------
> OS: Linux
> OS Version: #149~20.04.1-Ubuntu SMP Wed Apr 16 08:29:56 UTC 2025
> Python Version: 3.13.12 (main, Feb 12 2026, 00:45:41) [Clang 21.1.4 ]

Package Information
-------------------
> langchain_core: 1.5.4
> langchain: 1.3.15
> langsmith: 0.10.18
> deepagents: 0.7.6
> langchain_anthropic: 1.5.6
> langchain_google_genai: 4.3.3
> langchain_openai: 1.1.11
> langchain_protocol: 0.0.18
> langgraph_sdk: 0.4.2

Optional packages not installed
-------------------------------
> deepagents-cli

Other Dependencies
-------------------
> langgraph: 1.2.11
```

Contributor guide

Open the contributing guide

Research direction

Start in langgraph/_internal/_runnable.py at RunnableCallable.__init__ and inspect how KWARGS_CONFIG_KEYS is matched against annotations. Run the reproduction and review tests/test_deprecation.py::test_config_parameter_incorrect_typing, then add coverage for the future-import and RunnableConfig | None case. Done means the warning no longer contradicts its recommendation and config injection remains available for the reproduced annotation.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.