From 99ff594d5e9a9bcc162e5c6d4178a06bd5d7c1d2 Mon Sep 17 00:00:00 2001 From: Kiewan Villatel Date: Fri, 25 Sep 2026 10:14:13 +0200 Subject: [PATCH] fix(core): don't send a default project with agent-addressed runs When LANGSMITH_AGENT_ID / LANGSMITH_AGENT_ENVIRONMENT address runs to a LangSmith agent and no project is configured, LangChainTracer fell back to the invented "default" project and assigned it to every run. The run then carried both agent_id and session_name, which the API rejects with a 400, dropping the trace. With agent addressing active, fall back only to a project the user configured. The agent helpers are looked up with getattr since they only exist in langsmith>=0.14.0. Co-Authored-By: Claude Opus 5.5 (1M context) --- libs/core/langchain_core/tracers/langchain.py | 23 ++++++- .../unit_tests/tracers/test_langchain.py | 68 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/tracers/langchain.py b/libs/core/langchain_core/tracers/langchain.py index 954470e39c..7605b1c9ea 100644 --- a/libs/core/langchain_core/tracers/langchain.py +++ b/libs/core/langchain_core/tracers/langchain.py @@ -81,6 +81,27 @@ def get_client() -> Client: return rt.get_cached_client() +def _get_default_project_name() -> str | None: + """Get the project to trace to when none is passed to the tracer. + + When the environment addresses runs to a LangSmith agent + (`LANGSMITH_AGENT_ID` / `LANGSMITH_AGENT_ENVIRONMENT`), only a project the + user configured is returned. Falling back to the invented `'default'` + project would send it alongside the agent, and the API rejects a run + addressed to both. + + Agent addressing requires `langsmith>=0.14.0`; older versions have no agent + to address, so the helpers are looked up rather than imported. + """ + get_agent_id = getattr(ls_utils, "get_tracer_agent_id", None) + get_agent_environment = getattr(ls_utils, "get_tracer_agent_environment", None) + if (get_agent_id is not None and get_agent_id() is not None) or ( + get_agent_environment is not None and get_agent_environment() is not None + ): + return ls_utils.get_tracer_project(return_default_value=False) + return ls_utils.get_tracer_project() + + def _get_executor() -> ThreadPoolExecutor: """Get the executor.""" global _EXECUTOR # noqa: PLW0603 @@ -168,7 +189,7 @@ class LangChainTracer(BaseTracer): self.example_id = ( UUID(example_id) if isinstance(example_id, str) else example_id ) - self.project_name = project_name or ls_utils.get_tracer_project() + self.project_name = project_name or _get_default_project_name() self.client = client or get_client() self.tags = tags or [] self.latest_run: Run | None = None diff --git a/libs/core/tests/unit_tests/tracers/test_langchain.py b/libs/core/tests/unit_tests/tracers/test_langchain.py index 686e3c2194..753bdd2f7b 100644 --- a/libs/core/tests/unit_tests/tracers/test_langchain.py +++ b/libs/core/tests/unit_tests/tracers/test_langchain.py @@ -8,6 +8,7 @@ from uuid import UUID import pytest from langsmith import Client +from langsmith import utils as ls_utils from langsmith.run_trees import RunTree from langsmith.utils import get_env_var, get_tracer_project @@ -154,6 +155,73 @@ def test_correct_get_tracer_project( assert projects == [expected_project_name] +def _clear_tracer_env_caches() -> None: + getters: tuple[Any, ...] = ( + get_env_var, + get_tracer_project, + getattr(ls_utils, "get_tracer_agent_id", None), + getattr(ls_utils, "get_tracer_agent_environment", None), + ) + for getter in getters: + if hasattr(getter, "cache_clear"): + getter.cache_clear() + + +@pytest.mark.parametrize( + ("envvars", "expected_project_name"), + [ + ({"LANGSMITH_AGENT_ID": "my-agent"}, None), + ({"LANGSMITH_AGENT_ENVIRONMENT": "development"}, None), + ( + { + "LANGSMITH_AGENT_ID": "my-agent", + "LANGSMITH_AGENT_ENVIRONMENT": "development", + }, + None, + ), + ( + { + "LANGSMITH_AGENT_ID": "my-agent", + "LANGSMITH_AGENT_ENVIRONMENT": "development", + "LANGSMITH_PROJECT": "configured", + }, + "configured", + ), + ], + ids=[ + "no 'default' project with agent id", + "no 'default' project with agent environment", + "no 'default' project with agent pair", + "keep a configured project with agent pair", + ], +) +def test_tracer_project_with_agent_addressing( + envvars: dict[str, str], expected_project_name: str | None +) -> None: + if not hasattr(ls_utils, "get_tracer_agent_environment"): + pytest.skip("agent addressing requires langsmith>=0.14.0") + _clear_tracer_env_caches() + try: + with pytest.MonkeyPatch.context() as mp: + for k in ( + "LANGSMITH_PROJECT", + "LANGCHAIN_PROJECT", + "LANGCHAIN_SESSION", + "HOSTED_LANGSERVE_PROJECT_NAME", + "LANGSMITH_AGENT_ID", + "LANGSMITH_AGENT_ENVIRONMENT", + ): + mp.delenv(k, raising=False) + for k, v in envvars.items(): + mp.setenv(k, v) + + tracer = LangChainTracer(client=unittest.mock.MagicMock(spec=Client)) + assert tracer.project_name == expected_project_name + finally: + # The env getters are cached, so leave nothing behind for later tests. + _clear_tracer_env_caches() + + @pytest.mark.parametrize( ("generations", "expected"), [