fix(code): resolve editable SDK version metadata (#4394)

Editable installs now report dcode and SDK version metadata from the
same resolver used elsewhere, so diagnostics reflect the checked-out
source instead of stale package metadata. The stream config still keeps
the heavier resolver off the module import path and treats SDK version
reporting as best-effort metadata.
This commit is contained in:
Mason Daugherty
2026-06-30 16:54:16 -04:00
committed by GitHub
parent 1f8e8dc942
commit 3239bf4edf
2 changed files with 107 additions and 17 deletions
+37 -10
View File
@@ -14,7 +14,7 @@ import sys
import threading
from dataclasses import dataclass, field as dataclass_field
from enum import StrEnum
from importlib.metadata import PackageNotFoundError, distribution, version
from importlib.metadata import PackageNotFoundError, distribution
from pathlib import Path
from typing import TYPE_CHECKING, Any, Protocol
from urllib.parse import unquote, urlparse
@@ -980,20 +980,43 @@ Kept short so tracing metadata can never stall app flows.
def _get_deepagents_version() -> str | None:
"""Read the installed Deep Agents SDK version from package metadata.
"""Resolve the installed Deep Agents SDK version for diagnostics.
This intentionally calls `importlib.metadata.version` directly instead of
`resolve_sdk_version`: `config` is on the startup hot path, while
`resolve_sdk_version` lives in `extras_info` and imports `packaging`.
Editable installs can leave package metadata behind the source checkout, so
this uses the shared resolver that prefers the editable source version and
falls back to metadata when needed.
Returns:
The installed Deep Agents SDK version, or `None` when package metadata
is unavailable.
The resolved Deep Agents SDK version, or `None` when unavailable.
"""
# Imported lazily on purpose: `extras_info` pulls in `packaging`, which we
# keep off `config`'s module-import path (the startup hot path). Do not
# hoist this to the top of the module. The import is also guarded so a
# broken/absent `packaging` can never crash best-effort diagnostic metadata.
try:
return version("deepagents")
except PackageNotFoundError:
from deepagents_code.extras_info import resolve_sdk_version
sdk_version, status = resolve_sdk_version()
except ImportError:
logger.warning(
"Could not import resolve_sdk_version for SDK version metadata",
exc_info=True,
)
return None
return sdk_version if status == "resolved" else None
def _format_lc_version(base_version: str, *, editable: bool) -> str:
"""Format an `lc_versions` value with editable-install context.
Args:
base_version: The base version string.
editable: Whether the distribution is installed in editable mode.
Returns:
The version string, suffixed with ` (editable)` when `editable`.
"""
return f"{base_version} (editable)" if editable else base_version
def _resolve_editable_info() -> tuple[bool, str | None]:
@@ -1467,7 +1490,11 @@ def build_stream_config(
# Legacy / diagnostic keys preserved for backward-compatibility during the
# coding-agent-v1 rollout (not part of the contract).
metadata["lc_versions"] = {"deepagents-code": __version__}
metadata["lc_versions"] = {
"deepagents-code": _format_lc_version(
__version__, editable=_is_editable_install()
)
}
deepagents_version = _get_deepagents_version()
if deepagents_version is not None:
metadata["dcode_client_deepagents_version"] = deepagents_version
@@ -5,7 +5,6 @@ import sys
from asyncio import Future
from collections.abc import AsyncIterator, Awaitable, Callable, Generator
from datetime import datetime
from importlib.metadata import PackageNotFoundError
from io import StringIO
from pathlib import Path
from types import SimpleNamespace
@@ -804,12 +803,35 @@ class TestBuildStreamConfig:
"""CLI version should always be present in metadata.lc_versions."""
from deepagents_code._version import __version__
config = build_stream_config("t-ver", assistant_id=None)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch("deepagents_code.config._get_deepagents_version", return_value=None),
):
config = build_stream_config("t-ver", assistant_id=None)
assert config["metadata"]["lc_versions"] == {"deepagents-code": __version__}
def test_versions_marks_editable_cli_version(self) -> None:
"""Editable dcode installs should be visible in metadata.lc_versions."""
from deepagents_code._version import __version__
with (
patch("deepagents_code.config._is_editable_install", return_value=True),
patch("deepagents_code.config._get_deepagents_version", return_value=None),
):
config = build_stream_config("t-editable", assistant_id=None)
assert config["metadata"]["lc_versions"] == {
"deepagents-code": f"{__version__} (editable)"
}
def test_dcode_client_deepagents_version_is_diagnostic_metadata(self) -> None:
"""Client-side SDK version should not be reported as graph instrumentation."""
with patch("deepagents_code.config.version", return_value="1.2.3"):
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.extras_info.resolve_sdk_version",
return_value=("1.2.3", "resolved"),
),
):
config = build_stream_config("t-sdk", assistant_id=None)
assert config["metadata"]["dcode_client_deepagents_version"] == "1.2.3"
assert "deepagents" not in config["metadata"]["lc_versions"]
@@ -818,9 +840,12 @@ class TestBuildStreamConfig:
self,
) -> None:
"""Missing SDK metadata should not prevent stream config construction."""
with patch(
"deepagents_code.config.version",
side_effect=PackageNotFoundError("deepagents"),
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.extras_info.resolve_sdk_version",
return_value=(None, "not_installed"),
),
):
config = build_stream_config("t-missing-sdk", assistant_id=None)
@@ -835,7 +860,7 @@ class TestBuildStreamConfig:
if module == "deepagents" or module.startswith("deepagents."):
monkeypatch.delitem(sys.modules, module, raising=False)
with patch("deepagents_code.config.version", return_value="1.2.3"):
with patch("deepagents_code.config._is_editable_install", return_value=False):
build_stream_config("t-no-sdk-import", assistant_id=None)
assert not any(
@@ -843,6 +868,44 @@ class TestBuildStreamConfig:
for module in sys.modules
)
def test_get_deepagents_version_maps_status_to_value(self) -> None:
"""Only a `resolved` status yields a version; other statuses map to None.
The guard keys on `status`, not on the version string, so a non-resolved
status must drop even a non-`None` version the resolver flagged as
untrustworthy.
"""
from deepagents_code.config import _get_deepagents_version
with patch(
"deepagents_code.extras_info.resolve_sdk_version",
return_value=("1.2.3", "error"),
):
assert _get_deepagents_version() is None
with patch(
"deepagents_code.extras_info.resolve_sdk_version",
return_value=("1.2.3", "resolved"),
):
assert _get_deepagents_version() == "1.2.3"
def test_versions_editable_with_resolved_sdk_version(self) -> None:
"""Editable suffix and SDK diagnostic version are populated independently."""
from deepagents_code._version import __version__
with (
patch("deepagents_code.config._is_editable_install", return_value=True),
patch(
"deepagents_code.extras_info.resolve_sdk_version",
return_value=("1.2.3", "resolved"),
),
):
config = build_stream_config("t-editable-sdk", assistant_id=None)
assert config["metadata"]["lc_versions"] == {
"deepagents-code": f"{__version__} (editable)"
}
assert config["metadata"]["dcode_client_deepagents_version"] == "1.2.3"
def test_user_id_included_when_set(self) -> None:
"""DEEPAGENTS_CODE_USER_ID should appear in metadata when set."""
with patch.dict("os.environ", {"DEEPAGENTS_CODE_USER_ID": "mason"}):