fix: point View trace link at the correct tracing project (#1521)

* fix: resolve trace URL project id by tracing project name

Graphs were split into separate LangSmith tracing projects
(open-swe-agent, open-swe-review) but the "View trace" link still used
a single fixed project-id env var pointing at the old combined project.
Resolve the project id from the tracing project name so agent and
reviewer links point at their respective projects, falling back to the
env var when resolution is unavailable.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* docs: document per-graph tracing projects for trace links

View trace links now resolve project IDs from the open-swe-agent /
open-swe-review project names. Document this so fresh deployments create
the right projects instead of relying on a single project ID.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-13 09:37:15 -07:00 • committed by GitHub
parent 38dcc37040
commit 889b2663d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 125 additions and 8 deletions

View file

@ -129,6 +129,8 @@ Open SWE uses [LangSmith](https://smith.langchain.com/) for:
4. Get your **Tenant ID**: Visit LangSmith, login, then copy the UUID in the URL. Example: if your URL is `https://smith.langchain.com/o/72184268-01ea-4d29-98cc-6cfcf0f2abb0/agents/chat` -> the tenant ID would be `72184268-01ea-4d29-98cc-6cfcf0f2abb0`. Save it as `LANGSMITH_TENANT_ID_PROD`.
5. Get your **Project ID**: open your tracing project in LangSmith, then click on the **ID** button in the top left, directly next to the project name. Save it as `LANGSMITH_TRACING_PROJECT_ID_PROD`
> **Note on per-graph tracing projects.** The graphs trace into separate projects by name — `open-swe-agent` (main agent) and `open-swe-review` (reviewer/analyzer). "View trace" links resolve the correct project ID from these names automatically (via the `LANGSMITH_API_KEY_PROD` client), so make sure projects with these names exist in your tenant. If a name can't be resolved, links fall back to `LANGSMITH_TRACING_PROJECT_ID_PROD`, so set it to whichever project you want links to point at by default.
### 4b. Configure GitHub OAuth (optional but recommended)
This is the **agent-runtime** OAuth provider: it lets each agent run authenticate with the triggering user's own GitHub account, brokered by LangSmith. (It is separate from the dashboard-login OAuth, which uses `GITHUB_APP_CLIENT_ID`/`GITHUB_APP_CLIENT_SECRET` directly — see step 3c.) Without it, all agent operations use the GitHub App's installation token (a shared bot identity).
@ -399,7 +401,7 @@ LANGSMITH_API_KEY_PROD="" # From step 4a
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_PROJECT="" # LangSmith project name for traces
LANGSMITH_TENANT_ID_PROD=""
LANGSMITH_TRACING_PROJECT_ID_PROD=""
LANGSMITH_TRACING_PROJECT_ID_PROD="" # Fallback project ID for "View trace" links; graphs trace into the open-swe-agent / open-swe-review projects by name
LANGSMITH_URL_PROD="https://smith.langchain.com"
# === LLM ===

View file

@ -52,6 +52,7 @@ from ..utils.github_token import (
)
from ..utils.langsmith import get_langsmith_trace_url
from ..utils.slack import post_slack_thread_reply
from ..utils.tracing import REVIEW_TRACING_PROJECT
def publish_review(
@ -175,7 +176,7 @@ async def _resolve_review_trace_url(thread_id: str, config_override: object) ->
return None
if not thread_id:
return None
return get_langsmith_trace_url(thread_id)
return get_langsmith_trace_url(thread_id, project_name=REVIEW_TRACING_PROJECT)
def _is_reviewer_eval_mode(configurable: dict[str, Any]) -> bool:

View file

@ -10,25 +10,71 @@ from typing import Any
from langsmith import Client as LangSmithClient
from langsmith.utils import LangSmithNotFoundError
from .tracing import AGENT_TRACING_PROJECT
logger = logging.getLogger(__name__)
_PROJECT_ID_CACHE: dict[str, str] = {}
def _compose_langsmith_project_url() -> str:
"""Build the LangSmith project URL base from environment variables."""
def _build_prod_langsmith_client() -> LangSmithClient | None:
"""Build a LangSmith client scoped to the prod tenant for project lookups."""
api_key = (
os.environ.get("LANGSMITH_API_KEY_PROD")
or os.environ.get("LANGSMITH_API_KEY")
or os.environ.get("LANGCHAIN_API_KEY")
)
if not api_key:
return None
api_url = os.environ.get("LANGSMITH_ENDPOINT_PROD") or os.environ.get(
"LANGSMITH_ENDPOINT", "https://api.smith.langchain.com"
)
return LangSmithClient(api_key=api_key, api_url=api_url)
def _resolve_project_id_by_name(project_name: str) -> str | None:
"""Resolve a LangSmith project id from its name, caching successful lookups."""
cached = _PROJECT_ID_CACHE.get(project_name)
if cached:
return cached
client = _build_prod_langsmith_client()
if client is None:
return None
try:
project = client.read_project(project_name=project_name)
except LangSmithNotFoundError:
return None
except Exception: # noqa: BLE001
logger.warning("Failed to resolve LangSmith project id for %s", project_name, exc_info=True)
return None
project_id = getattr(project, "id", None)
if not project_id:
return None
resolved = str(project_id)
_PROJECT_ID_CACHE[project_name] = resolved
return resolved
def _compose_langsmith_project_url(project_name: str = AGENT_TRACING_PROJECT) -> str:
"""Build the LangSmith project URL base for a given tracing project name."""
host_url = os.environ.get("LANGSMITH_URL_PROD", "https://smith.langchain.com")
tenant_id = os.environ.get("LANGSMITH_TENANT_ID_PROD")
project_id = os.environ.get("LANGSMITH_TRACING_PROJECT_ID_PROD")
project_id = _resolve_project_id_by_name(project_name) or os.environ.get(
"LANGSMITH_TRACING_PROJECT_ID_PROD"
)
if not tenant_id or not project_id:
raise ValueError(
"LANGSMITH_TENANT_ID_PROD and LANGSMITH_TRACING_PROJECT_ID_PROD must be set"
"LANGSMITH_TENANT_ID_PROD must be set and the tracing project id must be resolvable"
)
return f"{host_url}/o/{tenant_id}/projects/p/{project_id}"
def get_langsmith_trace_url(thread_id: str) -> str | None:
def get_langsmith_trace_url(
thread_id: str, project_name: str = AGENT_TRACING_PROJECT
) -> str | None:
"""Build the LangSmith thread URL for a given thread ID."""
try:
project_url = _compose_langsmith_project_url()
project_url = _compose_langsmith_project_url(project_name)
return f"{project_url}/t/{thread_id}"
except Exception: # noqa: BLE001
logger.warning(

View file

@ -0,0 +1,68 @@
import pytest
from agent.utils import langsmith as ls_utils
from agent.utils.tracing import AGENT_TRACING_PROJECT, REVIEW_TRACING_PROJECT
@pytest.fixture(autouse=True)
def _clear_cache() -> None:
ls_utils._PROJECT_ID_CACHE.clear()
def _set_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LANGSMITH_URL_PROD", "https://smith.example")
monkeypatch.setenv("LANGSMITH_TENANT_ID_PROD", "tenant-1")
monkeypatch.delenv("LANGSMITH_TRACING_PROJECT_ID_PROD", raising=False)
def test_trace_url_resolves_project_id_by_name(monkeypatch: pytest.MonkeyPatch) -> None:
_set_env(monkeypatch)
monkeypatch.setattr(
ls_utils,
"_resolve_project_id_by_name",
lambda name: "agent-pid" if name == AGENT_TRACING_PROJECT else "review-pid",
)
agent_url = ls_utils.get_langsmith_trace_url("t1")
review_url = ls_utils.get_langsmith_trace_url("t2", project_name=REVIEW_TRACING_PROJECT)
assert agent_url == "https://smith.example/o/tenant-1/projects/p/agent-pid/t/t1"
assert review_url == "https://smith.example/o/tenant-1/projects/p/review-pid/t/t2"
def test_trace_url_falls_back_to_env_project_id(monkeypatch: pytest.MonkeyPatch) -> None:
_set_env(monkeypatch)
monkeypatch.setenv("LANGSMITH_TRACING_PROJECT_ID_PROD", "env-pid")
monkeypatch.setattr(ls_utils, "_resolve_project_id_by_name", lambda name: None)
url = ls_utils.get_langsmith_trace_url("t3")
assert url == "https://smith.example/o/tenant-1/projects/p/env-pid/t/t3"
def test_trace_url_none_when_unresolvable(monkeypatch: pytest.MonkeyPatch) -> None:
_set_env(monkeypatch)
monkeypatch.setattr(ls_utils, "_resolve_project_id_by_name", lambda name: None)
assert ls_utils.get_langsmith_trace_url("t4") is None
def test_resolve_project_id_caches_success(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
class _FakeProject:
id = "pid-123"
class _FakeClient:
def read_project(self, *, project_name: str) -> _FakeProject:
calls.append(project_name)
return _FakeProject()
monkeypatch.setattr(ls_utils, "_build_prod_langsmith_client", lambda: _FakeClient())
first = ls_utils._resolve_project_id_by_name(AGENT_TRACING_PROJECT)
second = ls_utils._resolve_project_id_by_name(AGENT_TRACING_PROJECT)
assert first == "pid-123"
assert second == "pid-123"
assert calls == [AGENT_TRACING_PROJECT]