feat(core): Update inheritance behavior for tracer metadata for special keys (#36900)

JS equivalent: https://github.com/langchain-ai/langchainjs/pull/10733
This commit is contained in:
Jacob Lee
2026-04-20 14:58:01 -07:00
committed by GitHub
parent 37f0b37f1c
commit 40026a7282
2 changed files with 84 additions and 2 deletions

View File

@@ -36,6 +36,22 @@ logger = logging.getLogger(__name__)
_LOGGED = set()
_EXECUTOR: ThreadPoolExecutor | None = None
OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS: frozenset[str] = frozenset(
{"ls_agent_type"}
)
"""Allowlist of LangSmith-only tracing metadata keys that bypass the default
"first wins" merge semantics used when propagating tracer metadata to nested
runs.
Keys in this set are ALWAYS overridden by the nearest enclosing tracer config,
so nested callers (e.g. a subagent) can replace a value inherited from an
ancestor.
Keep this list very small: every key here loses the default "first wins"
protection and is always clobbered by the nearest enclosing tracer config.
Only keys that are strictly for LangSmith tracing bookkeeping should be added.
"""
def log_error_once(method: str, exception: Exception) -> None:
"""Log an error once.
@@ -176,7 +192,16 @@ class LangChainTracer(BaseTracer):
else:
merged_metadata = dict(base_metadata)
for key, value in metadata.items():
if key not in merged_metadata:
# For allowlisted LangSmith-only inheritable metadata keys
# (e.g. ``ls_agent_type``), nested callers are allowed to
# OVERRIDE the value inherited from an ancestor. For all
# other keys we keep the existing "first wins" behavior so
# that ancestor-provided tracing metadata is not accidentally
# clobbered by child runs.
if (
key not in merged_metadata
or key in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS
):
merged_metadata[key] = value
merged_tags = sorted(set(self.tags + tags)) if tags else self.tags
@@ -448,7 +473,16 @@ def _patch_missing_metadata(self: LangChainTracer, run: Run) -> None:
metadata = run.metadata
patched = None
for k, v in self.tracing_metadata.items():
if k not in metadata:
# ``OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS`` are a small,
# LangSmith-only allowlist that bypasses the "first wins" merge
# so a nested caller (e.g. a subagent) can override a parent-set value.
if k not in metadata or k in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS:
# Skip the copy when the value already matches (avoids cloning
# the shared dict in the common "already set" case). Use a
# ``k in metadata`` guard so a legitimate missing key whose
# tracer value happens to be ``None`` is still patched in.
if k in metadata and metadata[k] == v:
continue
if patched is None:
# Copy on first miss to avoid mutating the shared dict.
patched = {**metadata}

View File

@@ -807,6 +807,28 @@ class TestPatchMissingMetadata:
assert run.metadata["env"] == "staging"
assert run.metadata["extra"] == "from_tracer"
def test_allowlisted_key_overrides_existing_run_metadata(self) -> None:
"""Allowlisted LangSmith keys override existing run metadata."""
tracer = self._make_tracer(metadata={"ls_agent_type": "subagent"})
run = self._make_run(metadata={"ls_agent_type": "root", "other": "keep"})
_patch_missing_metadata(tracer, run)
assert run.metadata["ls_agent_type"] == "subagent"
assert run.metadata["other"] == "keep"
def test_allowlisted_key_noop_when_values_match(self) -> None:
"""Allowlisted keys do not clone run metadata when the value is unchanged."""
original = {"ls_agent_type": "root"}
tracer = self._make_tracer(metadata={"ls_agent_type": "root"})
run = self._make_run(metadata=original)
_patch_missing_metadata(tracer, run)
# No-op: the shared dict should not be replaced with a copy.
assert run.extra["metadata"] is original
assert run.metadata == {"ls_agent_type": "root"}
class TestTracerMetadataCloning:
"""Tests for LangChainTracer metadata cloning helpers."""
@@ -901,3 +923,29 @@ class TestTracerMetadataCloning:
if copied.tracing_metadata is not None
}
assert copied_services == {"api", "worker"}
def test_copy_with_metadata_defaults_regular_keys_first_wins(self) -> None:
"""Regular (non-allowlisted) metadata keys keep "first wins" semantics."""
tracer = self._make_tracer(metadata={"env": "staging", "service": "orig"})
copied = tracer.copy_with_metadata_defaults(
metadata={"env": "prod", "service": "new"},
)
assert copied.tracing_metadata == {"env": "staging", "service": "orig"}
def test_copy_with_metadata_defaults_allowlisted_key_overrides(self) -> None:
"""Allowlisted LangSmith keys are overridden by nested caller metadata."""
tracer = self._make_tracer(
metadata={"ls_agent_type": "root", "env": "staging"},
)
copied = tracer.copy_with_metadata_defaults(
metadata={"ls_agent_type": "subagent", "env": "prod"},
)
# Allowlisted key is overridden, non-allowlisted keeps first-wins.
assert copied.tracing_metadata == {
"ls_agent_type": "subagent",
"env": "staging",
}