mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
fix(langgraph): merge configurable dicts across configs in ensure_config
Previously, when ensure_config was called with multiple configs (e.g.
Pregel.stream's ensure_config(self.config, config)), a later config's
configurable dict fully overwrote an earlier one. This caused values
bound via with_config({"configurable": {...}}) to be silently dropped
whenever the invoke-time config supplied any other configurable key.
Concretely, create_agent binds {configurable: {ls_agent_type: 'root'}}
via with_config, and any real invocation with a checkpointer supplies
{configurable: {thread_id: ...}} at invoke time. The bound ls_agent_type
was dropped, so no root-level runs were tagged with ls_agent_type='root'
in LangSmith.
Fix: merge the configurable dict across configs (stdlib merge_configs in
langchain_core already does this correctly; langgraph's merge_configs
helper in this same file also does this at line 109). Invoke-time values
still override bound values when keys collide.
Adds a regression test in tests/test_utils.py.
This commit is contained in:
@@ -306,7 +306,12 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||
for k, v in config.items():
|
||||
if _is_not_empty(v) and k in CONFIG_KEYS:
|
||||
if k == CONF:
|
||||
empty[k] = cast(dict, v).copy()
|
||||
# Merge configurable dicts across configs so that values
|
||||
# bound via `with_config(...)` (e.g. `ls_agent_type`) are
|
||||
# preserved when later configs (e.g. invoke-time) only
|
||||
# specify a subset of keys like `thread_id`.
|
||||
existing = cast(dict, empty.get(k) or {})
|
||||
empty[k] = {**existing, **cast(dict, v)}
|
||||
else:
|
||||
empty[k] = v # type: ignore[literal-required]
|
||||
for k, v in config.items():
|
||||
|
||||
@@ -427,3 +427,32 @@ def test_callback_manager_copies_configurable_ids_to_tracing_metadata() -> None:
|
||||
"thread_id": "th-123",
|
||||
"user_id": "uid-1",
|
||||
}
|
||||
|
||||
|
||||
def test_ensure_config_merges_configurable_across_configs() -> None:
|
||||
"""`ensure_config(bound, invoke_time)` should merge `configurable` dicts.
|
||||
|
||||
Prior to the fix, a later config's `configurable` dict fully overwrote an
|
||||
earlier one, causing values bound via `with_config({"configurable": {...}})`
|
||||
(e.g. `ls_agent_type="root"` set by `create_agent`) to be dropped whenever
|
||||
an invoke-time config supplied any other configurable key like `thread_id`.
|
||||
"""
|
||||
bound: RunnableConfig = {
|
||||
"configurable": {"ls_agent_type": "root", "custom_setting": "keep_me"},
|
||||
"metadata": {"ls_integration": "langchain_create_agent"},
|
||||
}
|
||||
invoke_time: RunnableConfig = {
|
||||
"configurable": {"thread_id": "t-1"},
|
||||
}
|
||||
merged = ensure_config(bound, invoke_time)
|
||||
# Both the bound and invoke-time configurable keys are preserved.
|
||||
assert merged["configurable"] == {
|
||||
"ls_agent_type": "root",
|
||||
"custom_setting": "keep_me",
|
||||
"thread_id": "t-1",
|
||||
}
|
||||
# Invoke-time values still override bound values when they collide.
|
||||
override: RunnableConfig = {"configurable": {"ls_agent_type": "subagent"}}
|
||||
merged2 = ensure_config(bound, override)
|
||||
assert merged2["configurable"]["ls_agent_type"] == "subagent"
|
||||
assert merged2["configurable"]["custom_setting"] == "keep_me"
|
||||
|
||||
Reference in New Issue
Block a user