fix(langgraph): merge lc_versions config metadata (#8052)

Preserve LangChain package-version trace metadata when graph-bound
config and invoke-time config both contribute `lc_versions`. The earlier
broad nested metadata merge has been narrowed to the LangChain-owned
`lc_versions` namespace, so arbitrary user metadata keeps the existing
last-writer-wins behavior.

## Changes

- Add a shared metadata merge path used by `merge_configs()` and
`ensure_config()` so top-level metadata keys are preserved across bound
and runtime configs.
- Special-case only `metadata["lc_versions"]` for one-level
package-version accumulation; duplicate package entries remain
last-writer-wins and non-mapping values still replace.
- Keep generic nested metadata maps, including user-owned
`metadata["versions"]`, as replacement-only to avoid changing arbitrary
metadata semantics.
- Raise the `langchain-core` lower bound to `>=1.4.7` so LangGraph’s
`lc_versions` handling aligns with the lc-core package-version
instrumentation.
- Cover both config merge helpers with tests for `lc_versions`
accumulation, non-recursive replacement within the package map, generic
nested metadata replacement, and defensive copying of mapping values.

## Test note

The stream event assertions for `test_imp_exception` now avoid depending
on leaked internal task-path metadata. With older `langchain-core`,
callback metadata could be mutated by later task runs, so every task
event in this test appeared to have the final task path index. That made
even the `task_with_exception` start event report
`metadata["langgraph_node"] == "my_task"`, which is inconsistent with
the event name.

`langchain-core>=1.4.6` preserves per-event metadata more accurately:
the first `my_task`, `task_with_exception`, and second `my_task` report
distinct task path indexes. The test now asserts the stable behavior
instead: event sequence, tags, required metadata, root stream payloads,
exception handling, and final outputs, without requiring the old leaked
task index.
This commit is contained in:
Mason Daugherty
2026-06-12 16:20:38 -04:00
committed by GitHub
parent 93307d6a42
commit 385033fd9c
10 changed files with 340 additions and 298 deletions
+46 -13
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections import ChainMap
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from os import getenv
from typing import Any, cast
@@ -79,6 +79,42 @@ def patch_checkpoint_map(
return config
def _copy_mapping_value(value: Any) -> Any:
return dict(value) if isinstance(value, Mapping) else value
def _merge_metadata(
base: Mapping[str, Any] | None, new: Mapping[str, Any] | None
) -> dict[str, Any]:
"""Merge metadata without mutating inputs.
Top-level keys merge with newer values winning. `lc_versions` is the only
mapping-valued key that merges one level deeper, so independent LangChain
packages can contribute package versions without changing generic metadata
semantics. Mapping values are copied one level, so deeper nested objects
remain shared. `None` inputs are treated as empty metadata.
Mirrors `langchain_core.runnables.config._merge_metadata_dicts` so configs
that pass through LangGraph's merge keep the same `lc_versions` semantics;
keep the two in sync. Unlike lc-core, this copies mapping values one level
(intentionally more defensive — do not "simplify" back to a shared ref).
"""
merged = {key: _copy_mapping_value(value) for key, value in (base or {}).items()}
for key, value in (new or {}).items():
if (
key == "lc_versions"
and isinstance(merged.get(key), Mapping)
and isinstance(value, Mapping)
):
merged[key] = {
**cast(Mapping[str, Any], merged[key]),
**value,
}
else:
merged[key] = _copy_mapping_value(value)
return merged
def _merge_callbacks(base: Callbacks, new: Callbacks) -> Callbacks:
"""Merge two callbacks values (None / list / BaseCallbackManager).
@@ -126,10 +162,10 @@ def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
if not value:
continue
if key == "metadata":
if base_value := base.get(key):
base[key] = {**base_value, **value} # type: ignore
else:
base[key] = value # type: ignore[literal-required]
base[key] = _merge_metadata(
cast(Mapping[str, Any] | None, base.get(key)),
cast(Mapping[str, Any], value),
)
elif key == "tags":
if base_value := base.get(key):
base[key] = [*base_value, *value] # type: ignore
@@ -327,14 +363,11 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
empty.get("callbacks"), cast(Callbacks, v)
)
elif k == "metadata":
# Shallow-merge metadata dicts across configs so values
# bound via with_config(...) (e.g. user_id) are preserved
# when later configs supply other metadata keys.
existing = empty.get("metadata")
empty["metadata"] = (
{**cast(dict, existing), **cast(dict, v)}
if existing
else cast(dict, v).copy()
# Matches merge_configs: top-level metadata keys merge, and
# only `lc_versions` merges one level deeper.
empty["metadata"] = _merge_metadata(
cast(Mapping[str, Any] | None, empty.get("metadata")),
cast(Mapping[str, Any], v),
)
elif k == "tags":
# Concatenate tags across configs so values bound via
+1 -1
View File
@@ -24,7 +24,7 @@ classifiers = [
'Programming Language :: Python :: 3.13',
]
dependencies = [
"langchain-core>=1.4.0,<2",
"langchain-core>=1.4.7,<2",
"langgraph-checkpoint>=4.1.0,<5.0.0",
"langgraph-sdk>=0.4.2,<0.5.0",
"langgraph-prebuilt>=1.1.0,<1.2.0",
+7
View File
@@ -9,6 +9,7 @@ import pytest
from langchain_core.messages import AIMessage, AnyMessage, ToolCall
from langchain_core.runnables import RunnableConfig, RunnableMap, RunnablePick
from langchain_core.tools import tool
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt.chat_agent_executor import create_react_agent
@@ -1399,6 +1400,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
(
@@ -1461,6 +1463,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
@@ -1513,6 +1516,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
@@ -6884,6 +6888,7 @@ def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
),
@@ -6911,6 +6916,7 @@ def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
),
@@ -6947,6 +6953,7 @@ def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
@@ -11,6 +11,7 @@ from typing import (
import pytest
from langchain_core.messages import AnyMessage, ToolCall
from langchain_core.runnables import RunnableConfig, RunnablePick
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.prebuilt.chat_agent_executor import create_react_agent
from langgraph.prebuilt.tool_node import ToolNode
@@ -1150,6 +1151,7 @@ async def test_prebuilt_tool_chat() -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
(
@@ -1212,6 +1214,7 @@ async def test_prebuilt_tool_chat() -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
@@ -1264,6 +1267,7 @@ async def test_prebuilt_tool_chat() -> None:
"ls_provider": "fakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
@@ -3981,6 +3985,7 @@ async def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
),
@@ -4008,6 +4013,7 @@ async def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
),
@@ -4044,6 +4050,7 @@ async def test_weather_subgraph(
"ls_provider": "fakemessageslistchatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
},
),
]
+2
View File
@@ -23,6 +23,7 @@ from langchain_core.runnables import (
RunnablePassthrough,
)
from langchain_core.runnables.graph import Edge
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.cache.base import BaseCache
from langgraph.checkpoint.base import (
BaseCheckpointSaver,
@@ -6923,6 +6924,7 @@ def test_tags_stream_mode_messages() -> None:
"ls_provider": "genericfakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
"tags": ["meow"],
},
)
+111 -272
View File
@@ -24,6 +24,7 @@ from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough
from langchain_core.utils.aiter import aclosing
from langchain_core.version import VERSION as LANGCHAIN_CORE_VERSION
from langgraph.cache.base import BaseCache
from langgraph.checkpoint.base import (
BaseCheckpointSaver,
@@ -7604,6 +7605,7 @@ async def test_tags_stream_mode_messages() -> None:
"ls_provider": "genericfakechatmodel",
"ls_model_type": "chat",
"ls_integration": "langchain_chat_model",
"lc_versions": {"langchain-core": LANGCHAIN_CORE_VERSION},
"tags": ["meow"],
},
)
@@ -8761,280 +8763,117 @@ async def test_imp_exception(
{"my_workflow": "done"},
]
assert [c async for c in my_workflow.astream_events(1, thread1)] == [
{
"event": "on_chain_start",
"data": {"input": 1},
"name": "LangGraph",
"tags": [],
"run_id": AnyStr(),
"metadata": {"thread_id": "1", "ls_integration": "langgraph"},
"parent_ids": [],
},
{
"event": "on_chain_start",
"data": {"input": 1},
"name": "my_workflow",
"tags": ["graph:step:4"],
"run_id": AnyStr(),
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_workflow",
"langgraph_triggers": ("__start__",),
"langgraph_path": ("__pregel_pull", "my_workflow"),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [AnyStr()],
},
{
"event": "on_chain_start",
"data": {"input": {"number": 1}},
"name": "my_task",
"tags": ["seq:step:1"],
"run_id": AnyStr(),
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "my_task",
"tags": ["seq:step:1"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"data": {"chunk": 2},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_end",
"data": {"output": 2, "input": {"number": 1}},
"run_id": AnyStr(),
"name": "my_task",
"tags": ["seq:step:1"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "LangGraph",
"tags": [],
"metadata": {"thread_id": "1", "ls_integration": "langgraph"},
"data": {"chunk": {"my_task": 2}},
"parent_ids": [],
},
{
"event": "on_chain_start",
"data": {"input": {"number": 1}},
"name": "task_with_exception",
"tags": ["seq:step:1"],
"run_id": AnyStr(),
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_start",
"data": {"input": {"number": 1}},
"name": "my_task",
"tags": ["seq:step:1"],
"run_id": AnyStr(),
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "my_task",
"tags": ["seq:step:1"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"data": {"chunk": 2},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_end",
"data": {"output": 2, "input": {"number": 1}},
"run_id": AnyStr(),
"name": "my_task",
"tags": ["seq:step:1"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_task",
"langgraph_triggers": ("__pregel_push",),
"langgraph_path": (
"__pregel_push",
("__pregel_pull", "my_workflow"),
2,
True,
),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [
AnyStr(),
AnyStr(),
],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "my_workflow",
"tags": ["graph:step:4"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_workflow",
"langgraph_triggers": ("__start__",),
"langgraph_path": ("__pregel_pull", "my_workflow"),
"langgraph_checkpoint_ns": AnyStr(),
},
"data": {"chunk": "done"},
"parent_ids": [AnyStr()],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "LangGraph",
"tags": [],
"metadata": {"thread_id": "1", "ls_integration": "langgraph"},
"data": {"chunk": {"my_task": 2}},
"parent_ids": [],
},
{
"event": "on_chain_end",
"data": {"output": "done", "input": 1},
"run_id": AnyStr(),
"name": "my_workflow",
"tags": ["graph:step:4"],
"metadata": {
"thread_id": "1",
"ls_integration": "langgraph",
"langgraph_step": 4,
"langgraph_node": "my_workflow",
"langgraph_triggers": ("__start__",),
"langgraph_path": ("__pregel_pull", "my_workflow"),
"langgraph_checkpoint_ns": AnyStr(),
},
"parent_ids": [AnyStr()],
},
{
"event": "on_chain_stream",
"run_id": AnyStr(),
"name": "LangGraph",
"tags": [],
"metadata": {"thread_id": "1", "ls_integration": "langgraph"},
"data": {"chunk": {"my_workflow": "done"}},
"parent_ids": [],
},
{
"event": "on_chain_end",
"data": {"output": "done"},
"run_id": AnyStr(),
"name": "LangGraph",
"tags": [],
"metadata": {"thread_id": "1", "ls_integration": "langgraph"},
"parent_ids": [],
},
events = [c async for c in my_workflow.astream_events(1, thread1)]
assert [(event["event"], event["name"]) for event in events] == [
("on_chain_start", "LangGraph"),
("on_chain_start", "my_workflow"),
("on_chain_start", "my_task"),
("on_chain_stream", "my_task"),
("on_chain_end", "my_task"),
("on_chain_stream", "LangGraph"),
("on_chain_start", "task_with_exception"),
("on_chain_start", "my_task"),
("on_chain_stream", "my_task"),
("on_chain_end", "my_task"),
("on_chain_stream", "my_workflow"),
("on_chain_stream", "LangGraph"),
("on_chain_end", "my_workflow"),
("on_chain_stream", "LangGraph"),
("on_chain_end", "LangGraph"),
]
root_events = [event for event in events if event["name"] == "LangGraph"]
for event in root_events:
assert event["tags"] == []
assert event["metadata"]["thread_id"] == "1"
assert event["metadata"]["ls_integration"] == "langgraph"
assert event["parent_ids"] == []
workflow_events = [event for event in events if event["name"] == "my_workflow"]
for event in workflow_events:
assert event["tags"] == ["graph:step:4"]
metadata = event["metadata"]
assert metadata["thread_id"] == "1"
assert metadata["ls_integration"] == "langgraph"
assert metadata["langgraph_step"] == 4
assert metadata["langgraph_node"] == "my_workflow"
assert metadata["langgraph_triggers"] == ("__start__",)
assert metadata["langgraph_path"] == ("__pregel_pull", "my_workflow")
assert isinstance(metadata["langgraph_checkpoint_ns"], str)
assert len(event["parent_ids"]) == 1
task_events = [event for event in events if event["name"] == "my_task"]
assert len(task_events) == 6
for event in task_events:
assert event["tags"] == ["seq:step:1"]
metadata = event["metadata"]
assert metadata["thread_id"] == "1"
assert metadata["ls_integration"] == "langgraph"
assert metadata["langgraph_step"] == 4
assert metadata["langgraph_node"] == "my_task"
assert metadata["langgraph_triggers"] == ("__pregel_push",)
assert metadata["langgraph_path"][:2] == (
"__pregel_push",
("__pregel_pull", "my_workflow"),
)
assert metadata["langgraph_path"][-1] is True
assert isinstance(metadata["langgraph_checkpoint_ns"], str)
assert len(event["parent_ids"]) == 2
task_with_exception_event = next(
event for event in events if event["name"] == "task_with_exception"
)
assert task_with_exception_event["event"] == "on_chain_start"
assert task_with_exception_event["data"] == {"input": {"number": 1}}
assert task_with_exception_event["tags"] == ["seq:step:1"]
metadata = task_with_exception_event["metadata"]
assert metadata["thread_id"] == "1"
assert metadata["ls_integration"] == "langgraph"
assert metadata["langgraph_step"] == 4
assert metadata["langgraph_node"] == "task_with_exception"
assert metadata["langgraph_triggers"] == ("__pregel_push",)
assert metadata["langgraph_path"][:2] == (
"__pregel_push",
("__pregel_pull", "my_workflow"),
)
assert metadata["langgraph_path"][-1] is True
assert isinstance(metadata["langgraph_checkpoint_ns"], str)
assert len(task_with_exception_event["parent_ids"]) == 2
assert [
event["data"]
for event in events
if event["event"] == "on_chain_stream" and event["name"] == "LangGraph"
] == [
{"chunk": {"my_task": 2}},
{"chunk": {"my_task": 2}},
{"chunk": {"my_workflow": "done"}},
]
assert [
event["data"]
for event in events
if event["event"] == "on_chain_stream" and event["name"] == "my_task"
] == [{"chunk": 2}, {"chunk": 2}]
assert [
event["data"]
for event in events
if event["event"] == "on_chain_end" and event["name"] == "my_task"
] == [
{"output": 2, "input": {"number": 1}},
{"output": 2, "input": {"number": 1}},
]
assert [
event["data"]
for event in events
if event["event"] == "on_chain_stream" and event["name"] == "my_workflow"
] == [{"chunk": "done"}]
assert [
event["data"]
for event in events
if event["event"] == "on_chain_end" and event["name"] == "my_workflow"
] == [{"output": "done", "input": 1}]
assert events[-1]["data"] == {"output": "done"}
@pytest.mark.parametrize("with_timeout", [False, "inner", "outer", "both"])
@pytest.mark.parametrize("subgraph_persist", [True, False])
+154
View File
@@ -25,6 +25,7 @@ from langgraph._internal._config import (
_merge_callbacks,
ensure_config,
get_callback_manager_for_config,
merge_configs,
)
from langgraph._internal._fields import (
_is_optional_type,
@@ -520,6 +521,159 @@ def test_ensure_config_metadata_later_wins_per_key() -> None:
assert merged["metadata"]["shared"] == "from_b"
def test_merge_configs_merges_metadata_lc_versions() -> None:
a = {
"metadata": {
"lc_versions": {"langgraph": "1.2.4"},
"lc_agent_name": "agent",
}
}
b = {"metadata": {"lc_versions": {"langchain-core": "1.2.0"}}}
merged = merge_configs(a, b)
assert merged["metadata"]["lc_versions"] == {
"langgraph": "1.2.4",
"langchain-core": "1.2.0",
}
assert merged["metadata"]["lc_agent_name"] == "agent"
def test_ensure_config_merges_metadata_lc_versions() -> None:
a = {
"metadata": {
"lc_versions": {"langgraph": "1.2.4"},
"lc_agent_name": "agent",
}
}
b = {"metadata": {"lc_versions": {"langchain-core": "1.2.0"}}}
merged = ensure_config(a, b)
assert merged["metadata"]["lc_versions"] == {
"langgraph": "1.2.4",
"langchain-core": "1.2.0",
}
assert merged["metadata"]["lc_agent_name"] == "agent"
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_lc_versions_later_values_win_without_recursive_merge(
merge: Callable[..., RunnableConfig],
) -> None:
a = {
"metadata": {
"lc_versions": {
"langgraph": "1.2.4",
"nested": {"only_a": "A", "shared": "from_a"},
}
}
}
b = {
"metadata": {
"lc_versions": {
"langchain-core": "1.2.0",
"nested": {"only_b": "B", "shared": "from_b"},
}
}
}
merged = merge(a, b)
assert merged["metadata"]["lc_versions"] == {
"langgraph": "1.2.4",
"langchain-core": "1.2.0",
"nested": {"only_b": "B", "shared": "from_b"},
}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_nested_mappings_other_than_lc_versions_are_replaced(
merge: Callable[..., RunnableConfig],
) -> None:
a = {"metadata": {"versions": {"langgraph": "1.2.4"}, "mode": "bound"}}
b = {
"metadata": {
"versions": {"langchain-core": "1.2.0"},
"mode": {"source": "runtime"},
}
}
merged = merge(a, b)
assert merged["metadata"]["versions"] == {"langchain-core": "1.2.0"}
assert merged["metadata"]["mode"] == {"source": "runtime"}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_non_mapping_values_later_wins(
merge: Callable[..., RunnableConfig],
) -> None:
a = {"metadata": {"lc_versions": {"langgraph": "1.2.4"}, "mode": "bound"}}
b = {"metadata": {"lc_versions": "runtime", "mode": {"source": "runtime"}}}
merged = merge(a, b)
assert merged["metadata"]["lc_versions"] == "runtime"
assert merged["metadata"]["mode"] == {"source": "runtime"}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_lc_versions_merge_does_not_mutate_inputs(
merge: Callable[..., RunnableConfig],
) -> None:
a_versions = {"langgraph": "1.2.4"}
b_versions = {"langchain-core": "1.2.0"}
a = {"metadata": {"lc_versions": a_versions}}
b = {"metadata": {"lc_versions": b_versions}}
merged = merge(a, b)
assert a == {"metadata": {"lc_versions": {"langgraph": "1.2.4"}}}
assert b == {"metadata": {"lc_versions": {"langchain-core": "1.2.0"}}}
assert merged["metadata"]["lc_versions"] is not a_versions
assert merged["metadata"]["lc_versions"] is not b_versions
merged["metadata"]["lc_versions"]["langgraph"] = "changed"
assert a_versions == {"langgraph": "1.2.4"}
assert b_versions == {"langchain-core": "1.2.0"}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_single_sided_mapping_values_are_copied(
merge: Callable[..., RunnableConfig],
) -> None:
base_versions = {"langgraph": "1.2.4"}
new_versions = {"langchain-core": "1.2.0"}
merged_base_only = merge({"metadata": {"lc_versions": base_versions}})
merged_new_only = merge(
{"metadata": {}},
{"metadata": {"lc_versions": new_versions}},
)
assert merged_base_only["metadata"]["lc_versions"] is not base_versions
assert merged_new_only["metadata"]["lc_versions"] is not new_versions
merged_base_only["metadata"]["lc_versions"]["langgraph"] = "changed"
merged_new_only["metadata"]["lc_versions"]["langchain-core"] = "changed"
assert base_versions == {"langgraph": "1.2.4"}
assert new_versions == {"langchain-core": "1.2.0"}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_empty_incoming_preserves_base_lc_versions(
merge: Callable[..., RunnableConfig],
) -> None:
a = {"metadata": {"lc_versions": {"langgraph": "1.2.4"}}}
b = {"metadata": {"lc_versions": {}}}
merged = merge(a, b)
assert merged["metadata"]["lc_versions"] == {"langgraph": "1.2.4"}
@pytest.mark.parametrize("merge", [merge_configs, ensure_config])
def test_metadata_lc_versions_accumulate_across_more_than_two_configs(
merge: Callable[..., RunnableConfig],
) -> None:
a = {"metadata": {"lc_versions": {"langgraph": "1.2.4"}}}
b = {"metadata": {"lc_versions": {"langchain-core": "1.2.0"}}}
c = {"metadata": {"lc_versions": {"langchain": "1.1.0"}}}
merged = merge(a, b, c)
assert merged["metadata"]["lc_versions"] == {
"langgraph": "1.2.4",
"langchain-core": "1.2.0",
"langchain": "1.1.0",
}
def test_ensure_config_merges_tags_across_configs() -> None:
a = {"tags": ["alpha"]}
b = {"tags": ["beta"]}
+4 -4
View File
@@ -1350,7 +1350,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.4.0"
version = "1.4.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -1363,9 +1363,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/2b/fffaff399d20a56d40b9562fa19701e91abd72d8c9d9bc8c2673077b56b6/langchain_core-1.4.7.tar.gz", hash = "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", size = 952522, upload-time = "2026-06-12T19:23:57.588Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
{ url = "https://files.pythonhosted.org/packages/de/3e/dcdffa60078ae7b3a00ebb4cbbf1a204a14c3609983c604886523a7d4418/langchain_core-1.4.7-py3-none-any.whl", hash = "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d", size = 554941, upload-time = "2026-06-12T19:23:55.826Z" },
]
[[package]]
@@ -1454,7 +1454,7 @@ test = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
{ name = "langchain-core", specifier = ">=1.4.7,<2" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-prebuilt", editable = "../prebuilt" },
{ name = "langgraph-sdk", editable = "../sdk-py" },
+4 -4
View File
@@ -253,7 +253,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.4.0"
version = "1.4.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -266,9 +266,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/2b/fffaff399d20a56d40b9562fa19701e91abd72d8c9d9bc8c2673077b56b6/langchain_core-1.4.7.tar.gz", hash = "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", size = 952522, upload-time = "2026-06-12T19:23:57.588Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
{ url = "https://files.pythonhosted.org/packages/de/3e/dcdffa60078ae7b3a00ebb4cbbf1a204a14c3609983c604886523a7d4418/langchain_core-1.4.7-py3-none-any.whl", hash = "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d", size = 554941, upload-time = "2026-06-12T19:23:55.826Z" },
]
[[package]]
@@ -298,7 +298,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
{ name = "langchain-core", specifier = ">=1.4.7,<2" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-prebuilt", editable = "." },
{ name = "langgraph-sdk", editable = "../sdk-py" },
+4 -4
View File
@@ -266,7 +266,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.4.0"
version = "1.4.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -279,9 +279,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/2b/fffaff399d20a56d40b9562fa19701e91abd72d8c9d9bc8c2673077b56b6/langchain_core-1.4.7.tar.gz", hash = "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", size = 952522, upload-time = "2026-06-12T19:23:57.588Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
{ url = "https://files.pythonhosted.org/packages/de/3e/dcdffa60078ae7b3a00ebb4cbbf1a204a14c3609983c604886523a7d4418/langchain_core-1.4.7-py3-none-any.whl", hash = "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d", size = 554941, upload-time = "2026-06-12T19:23:55.826Z" },
]
[[package]]
@@ -311,7 +311,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
{ name = "langchain-core", specifier = ">=1.4.7,<2" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "langgraph-prebuilt", editable = "../prebuilt" },
{ name = "langgraph-sdk", editable = "." },