mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): pin app version when installing extras (#4313)
Installing a provider extra rebuilds the `dcode` uv tool environment. That rebuild should keep the currently running app version, not let uv backtrack to an older `deepagents-code` release when the latest stable app depends on prerelease packages. ## Changes - Pin `_install_extra_uv_tool_command` and `install_package_command` to the running `deepagents-code` version and pass `--prerelease allow` for dependency resolution. - Keep malformed app version pins as `ValueError` while preserving the existing `ExtrasIntrospectionError` path for invalid installed-extra metadata. - Update command-builder and executor coverage for pinned extra/package installs, prerelease dependency resolution, and invalid version handling.
This commit is contained in:
@@ -2123,6 +2123,7 @@ def _uv_tool_install_command(
|
||||
Raises:
|
||||
ExtrasIntrospectionError: If a metadata-sourced extra name fails PEP 508
|
||||
validation.
|
||||
ValueError: If `version` is not PEP 440 compliant.
|
||||
|
||||
Propagates `ToolRequirementIntrospectionError` if the uv tool receipt's
|
||||
interpreter or `--with` packages cannot be determined safely from the tool
|
||||
@@ -2138,6 +2139,8 @@ def _uv_tool_install_command(
|
||||
try:
|
||||
requirement = _dcode_extras_requirement(extras, version=version)
|
||||
except ValueError as exc:
|
||||
if str(exc).startswith("Invalid deepagents-code version"):
|
||||
raise
|
||||
msg = f"Distribution metadata yielded an invalid extra name: {exc}"
|
||||
raise ExtrasIntrospectionError(msg) from exc
|
||||
cmd = "uv tool install --reinstall -U" if reinstall else "uv tool install -U"
|
||||
@@ -2298,13 +2301,13 @@ def install_package_command(
|
||||
|
||||
Delegates to `_uv_tool_install_command` (the same builder the extras path
|
||||
uses), passing the new package as a `--with` requirement. That builder folds
|
||||
already-installed extras into the `deepagents-code[...]` requirement, and
|
||||
preserves the uv-managed Python interpreter, the receipt's existing `--with`
|
||||
packages, and the installed pre-release channel. Without this, reinstalling
|
||||
to add a second package would replace the tool with a plain `deepagents-code`
|
||||
(dropping extras the user added through `/install <extra>`), rebuild with
|
||||
only the newest `--with` package (dropping previously configured custom
|
||||
providers), or silently downgrade a pre-release install to the latest stable.
|
||||
already-installed extras into the pinned `deepagents-code[...]` requirement,
|
||||
and preserves the uv-managed Python interpreter and the receipt's existing
|
||||
`--with` packages. Without this, reinstalling to add a second package would
|
||||
replace the tool with a plain `deepagents-code` (dropping extras the user
|
||||
added through `/install <extra>`), rebuild with only the newest `--with`
|
||||
package (dropping previously configured custom providers), or silently
|
||||
downgrade when the latest stable app depends on prerelease packages.
|
||||
|
||||
Like the extras path (`_install_extra_uv_tool_command`), passes
|
||||
`reinstall=True` so the upgrade rebuilds the tool environment cleanly; see
|
||||
@@ -2335,8 +2338,8 @@ def install_package_command(
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return _uv_tool_install_command(
|
||||
version=None,
|
||||
include_prereleases=None,
|
||||
version=__version__,
|
||||
include_prereleases=True,
|
||||
distribution_name=distribution_name,
|
||||
with_packages_to_add=(package,),
|
||||
reinstall=True,
|
||||
@@ -2434,6 +2437,8 @@ def _install_extra_uv_tool_command(
|
||||
) -> str:
|
||||
"""Return the receipt-preserving uv command that installs one dcode extra.
|
||||
|
||||
Pins the running `deepagents-code` version and allows prerelease dependency
|
||||
resolution so adding an extra cannot make uv backtrack to an older app release.
|
||||
Passes `reinstall=True` so the upgrade rebuilds the tool environment from
|
||||
scratch rather than patching it in place; see `_uv_tool_install_command`'s
|
||||
`reinstall` parameter for why an in-place upgrade is unsafe.
|
||||
@@ -2459,8 +2464,8 @@ def _install_extra_uv_tool_command(
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return _uv_tool_install_command(
|
||||
version=None,
|
||||
include_prereleases=None,
|
||||
version=__version__,
|
||||
include_prereleases=True,
|
||||
distribution_name=distribution_name,
|
||||
extras_to_add=(extra,),
|
||||
reinstall=True,
|
||||
@@ -2504,11 +2509,11 @@ async def perform_install_extra(
|
||||
) -> tuple[bool, str]:
|
||||
"""Add `extra` to the installed dcode tool environment.
|
||||
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]'`,
|
||||
preserving any extras that are already installed. Editable installs are
|
||||
refused — the caller should rerun their `uv tool install --editable` command
|
||||
with `--with 'deepagents-code[<extra>]'` added so the extra is resolved
|
||||
against the editable source.
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]==<current>'
|
||||
--prerelease allow`, preserving any extras that are already installed.
|
||||
Editable installs are refused — the caller should rerun their
|
||||
`uv tool install --editable` command with `--with 'deepagents-code[<extra>]'`
|
||||
added so the extra is resolved against the editable source.
|
||||
|
||||
Args:
|
||||
extra: The extra name to install. Must satisfy `is_valid_extra_name`;
|
||||
@@ -2575,10 +2580,11 @@ async def perform_install_package(
|
||||
) -> tuple[bool, str]:
|
||||
"""Add an arbitrary `package` to the installed dcode tool environment.
|
||||
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]' --with
|
||||
<package>`, the escape hatch for a provider whose package is not a
|
||||
`deepagents-code` extra (e.g. a custom or in-house `class_path` model).
|
||||
Already-installed extras are preserved so the reinstall does not drop them.
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]==<current>'
|
||||
--with <package> --prerelease allow`, the escape hatch for a provider whose
|
||||
package is not a `deepagents-code` extra (e.g. a custom or in-house
|
||||
`class_path` model). Already-installed extras are preserved so the reinstall
|
||||
does not drop them.
|
||||
Editable installs are refused
|
||||
— the caller should rerun their `uv tool install --editable` command with
|
||||
`--with <package>` added so it resolves against the editable source.
|
||||
|
||||
@@ -33,7 +33,7 @@ from textual.screen import ModalScreen
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Checkbox, Input, Static
|
||||
|
||||
from deepagents_code._version import CHANGELOG_URL
|
||||
from deepagents_code._version import CHANGELOG_URL, __version__
|
||||
from deepagents_code.app import (
|
||||
_DEEPAGENTS_IMPORT_LOCK,
|
||||
_TYPING_IDLE_THRESHOLD_SECONDS,
|
||||
@@ -9161,8 +9161,9 @@ class TestDeferredActions:
|
||||
assert isinstance(widget, ErrorMessage)
|
||||
rendered = str(widget._content)
|
||||
assert (
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-custom_provider" in rendered
|
||||
"uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} "
|
||||
"--with langchain-custom_provider --prerelease allow" in rendered
|
||||
)
|
||||
assert "/model custom_provider:<model>" in rendered
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
from deepagents_code import _git as git_module, model_config
|
||||
from deepagents_code._env_vars import SERVER_ENV_PREFIX
|
||||
from deepagents_code._version import __version__
|
||||
from deepagents_code.config import (
|
||||
CLI_MAX_RETRIES_KEY,
|
||||
RECOMMENDED_SAFE_SHELL_COMMANDS,
|
||||
@@ -3929,8 +3930,9 @@ class TestCreateModelViaInitImportError:
|
||||
pytest.raises(
|
||||
ModelConfigError,
|
||||
match=(
|
||||
"Install with: uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-custom_provider"
|
||||
"Install with: uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} "
|
||||
"--with langchain-custom_provider --prerelease allow"
|
||||
),
|
||||
),
|
||||
):
|
||||
|
||||
@@ -2848,7 +2848,8 @@ class TestInstallExtraCommand:
|
||||
|
||||
assert install_extra_recovery_command("quickjs") == (
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[nvidia,quickjs]' --with langchain-custom"
|
||||
f"'deepagents-code[nvidia,quickjs]=={__version__}' "
|
||||
"--with langchain-custom --prerelease allow"
|
||||
)
|
||||
|
||||
def test_recovery_command_uses_script_for_non_uv_without_receipt(
|
||||
@@ -2903,11 +2904,11 @@ class TestInstallExtraCommand:
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert installed_extra_names("deepagents-code") == {"nvidia"}
|
||||
assert (
|
||||
_install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
|
||||
assert _install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"'deepagents-code[baseten,nvidia]=={__version__}' --prerelease allow"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_dedupes_existing_extra(
|
||||
@@ -2924,11 +2925,11 @@ class TestInstallExtraCommand:
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert (
|
||||
_install_extra_uv_tool_command(
|
||||
"nvidia", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U 'deepagents-code[nvidia]'"
|
||||
assert _install_extra_uv_tool_command(
|
||||
"nvidia", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"'deepagents-code[nvidia]=={__version__}' --prerelease allow"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_drops_composite_extras(
|
||||
@@ -2950,11 +2951,11 @@ class TestInstallExtraCommand:
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert installed_extra_names("deepagents-code") == {"nvidia"}
|
||||
assert (
|
||||
_install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
|
||||
assert _install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"'deepagents-code[baseten,nvidia]=={__version__}' --prerelease allow"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_preserves_receipt_python_and_with_packages(
|
||||
@@ -2981,7 +2982,8 @@ class TestInstallExtraCommand:
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[baseten,nvidia]' --with langchain-custom"
|
||||
f"'deepagents-code[baseten,nvidia]=={__version__}' "
|
||||
"--with langchain-custom --prerelease allow"
|
||||
)
|
||||
|
||||
def test_sorts_extras_deterministically(self) -> None:
|
||||
@@ -3018,7 +3020,7 @@ class TestInstallPackageCommand:
|
||||
"""`install_package_command` builds a uv tool package install string."""
|
||||
|
||||
def test_basic_no_extras(self, tmp_path, monkeypatch) -> None:
|
||||
"""Clean metadata with no installed extras yields a plain requirement."""
|
||||
"""Clean metadata with no extras yields the version-pinned requirement."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(
|
||||
tmp_path,
|
||||
@@ -3028,11 +3030,12 @@ class TestInstallPackageCommand:
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert (
|
||||
install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
assert install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} --with langchain-custom "
|
||||
"--prerelease allow"
|
||||
)
|
||||
|
||||
def test_allows_pep508_name_separators(self, tmp_path, monkeypatch) -> None:
|
||||
@@ -3045,12 +3048,12 @@ class TestInstallPackageCommand:
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert (
|
||||
install_package_command(
|
||||
"langchain.custom_provider", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain.custom_provider"
|
||||
assert install_package_command(
|
||||
"langchain.custom_provider", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} --with langchain.custom_provider "
|
||||
"--prerelease allow"
|
||||
)
|
||||
|
||||
def test_preserves_installed_extras(self, tmp_path, monkeypatch) -> None:
|
||||
@@ -3069,12 +3072,12 @@ class TestInstallPackageCommand:
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert installed_extra_names("deepagents-code") == {"nvidia"}
|
||||
assert (
|
||||
install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install --reinstall -U 'deepagents-code[nvidia]' "
|
||||
"--with langchain-custom"
|
||||
assert install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
) == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"'deepagents-code[nvidia]=={__version__}' --with langchain-custom "
|
||||
"--prerelease allow"
|
||||
)
|
||||
|
||||
def test_preserves_receipt_python_and_with_packages(
|
||||
@@ -3101,8 +3104,8 @@ class TestInstallPackageCommand:
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[nvidia]' --with langchain-first "
|
||||
"--with langchain-second"
|
||||
f"'deepagents-code[nvidia]=={__version__}' --with langchain-first "
|
||||
"--with langchain-second --prerelease allow"
|
||||
)
|
||||
|
||||
def test_does_not_duplicate_existing_receipt_package(
|
||||
@@ -3121,18 +3124,14 @@ class TestInstallPackageCommand:
|
||||
"LangChain_Custom", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert (
|
||||
command
|
||||
== "uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} --with langchain-custom "
|
||||
"--prerelease allow"
|
||||
)
|
||||
|
||||
def test_preserves_prerelease_channel(self, tmp_path, monkeypatch) -> None:
|
||||
"""Adding a package to a pre-release install keeps the pre-release channel.
|
||||
|
||||
The `--reinstall` rebuild re-resolves the unpinned `deepagents-code`
|
||||
requirement from scratch; without `--prerelease allow` uv would resolve
|
||||
to the latest stable and silently downgrade a pre-release user.
|
||||
"""
|
||||
def test_pins_prerelease_app_version(self, tmp_path, monkeypatch) -> None:
|
||||
"""Adding a package to a pre-release install keeps that exact app version."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
@@ -3144,17 +3143,14 @@ class TestInstallPackageCommand:
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"uv tool install --reinstall -U deepagents-code==1.0.0a1 "
|
||||
"--with langchain-custom --prerelease allow"
|
||||
)
|
||||
|
||||
def test_stable_channel_omits_prerelease_flag(self, tmp_path, monkeypatch) -> None:
|
||||
"""A stable install does not add `--prerelease allow` for a package add.
|
||||
|
||||
The negative companion to `test_preserves_prerelease_channel`: pins that
|
||||
the channel is *inferred* from the installed version, so an inverted
|
||||
condition that always emitted `--prerelease allow` would fail here.
|
||||
"""
|
||||
def test_stable_install_pins_app_and_allows_prerelease_deps(
|
||||
self, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""A stable app reinstall keeps the app pinned while allowing rc deps."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
@@ -3166,7 +3162,8 @@ class TestInstallPackageCommand:
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
"uv tool install --reinstall -U deepagents-code==1.0.0 "
|
||||
"--with langchain-custom --prerelease allow"
|
||||
)
|
||||
|
||||
def test_appends_new_with_package_after_sorted_receipt_packages(
|
||||
@@ -3193,8 +3190,9 @@ class TestInstallPackageCommand:
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-zeta --with langchain-alpha"
|
||||
"uv tool install --reinstall -U "
|
||||
f"deepagents-code=={__version__} --with langchain-zeta "
|
||||
"--with langchain-alpha --prerelease allow"
|
||||
)
|
||||
|
||||
def test_unpreservable_receipt_with_requirement_raises(
|
||||
@@ -3413,8 +3411,8 @@ class TestIsValidPackageName:
|
||||
def test_rejects_option_injection_leading_dash(self) -> None:
|
||||
"""A leading dash would smuggle uv options into `--with <name>`.
|
||||
|
||||
The command is `uv tool install --reinstall -U deepagents-code --with
|
||||
<name>`; a name
|
||||
The command is `uv tool install --reinstall -U deepagents-code==<version>
|
||||
--with <name>`; a name
|
||||
like `-rreqs.txt` or `--editable` would be parsed by uv as a flag, not a
|
||||
package. The validator must reject these regardless of `--force`/`--yes`.
|
||||
"""
|
||||
@@ -3598,6 +3596,27 @@ class TestPerformInstallPackage:
|
||||
assert "non-table requirement" in output
|
||||
assert "uv receipt" in caplog.text
|
||||
|
||||
async def test_invalid_app_version_is_reported(self, tmp_path, monkeypatch) -> None:
|
||||
"""A malformed app version pin is reported instead of escaping."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
with (
|
||||
patch(
|
||||
"deepagents_code.update_check.detect_install_method",
|
||||
return_value="uv",
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.update_check.shutil.which",
|
||||
return_value="/usr/bin/uv",
|
||||
),
|
||||
patch("deepagents_code.update_check.__version__", "not-a-version"),
|
||||
):
|
||||
success, output = await perform_install_package("langchain-custom")
|
||||
assert success is False
|
||||
assert "ValueError" in output
|
||||
assert "Invalid deepagents-code version" in output
|
||||
|
||||
|
||||
class TestRunInstallSubprocessFailureModes:
|
||||
"""Failure-mode coverage routed through `perform_install_extra`.
|
||||
|
||||
Reference in New Issue
Block a user