feat(code): /install optional extras (#3606)

Adds a first-class way to install optional extras into an existing
`dcode` install without dropping back to a shell or guessing package
names. Surfaces it as both the `/install <extra>` slash command (in-app)
and a `dcode --install EXTRA` headless flag, and rewrites the ecosystem
of `pip install …` recovery hints to point at the new flow.
This commit is contained in:
Mason Daugherty
2026-05-26 20:49:25 -04:00
committed by GitHub
parent 61f7753b8d
commit 7ffaa93dca
28 changed files with 1459 additions and 103 deletions
+2 -1
View File
@@ -6,7 +6,7 @@ Canonical list of slash commands for `deepagents-code`, derived from
regenerate after editing the registry.
## Public (25)
## Public (26)
| Command | Aliases | Description |
| --- | --- | --- |
@@ -21,6 +21,7 @@ regenerate after editing the registry.
| `/feedback` | | Submit a bug report or feature request |
| `/force-clear` | | Interrupt active work, clear chat, and start new thread |
| `/help` | | Show help |
| `/install` | | Install an optional extra (e.g. quickjs, daytona, fireworks) |
| `/mcp` | | Show MCP servers; `/mcp login <server>` to authenticate, `/mcp reconnect` to load deferred logins, F2 in the viewer to disable/enable a server |
| `/model` | | Switch or configure model (--model-params, --default) |
| `/notifications` | | Configure startup warning preferences |
+162 -10
View File
@@ -1478,7 +1478,7 @@ class DeepAgentsApp(App):
"""The exception itself when startup failed with
`MissingProviderPackageError`; `None` otherwise. Stashing the exception
rather than a tuple gives the hint builder named access to `.provider`
and `.package`, and gates the `pip install` / `/model` recovery hint
and `.package`, and gates the `/install` / `/model` recovery hint
without string-matching on the formatted error.
"""
@@ -2653,12 +2653,29 @@ class DeepAgentsApp(App):
and self._server_kwargs is not None
):
missing = self._server_startup_missing_provider_package
text += (
f"\n\nHint: install the package with "
f"`pip install {missing.package}`, then run "
f"`/model {missing.provider}:<model>` to retry. "
f"Or pick a different provider with `/model`."
)
from deepagents_code.extras_info import extra_for_package
extra = extra_for_package(missing.package)
if extra is not None:
text += (
f"\n\nHint: install the package with `/install {extra}`, "
f"then run `/model {missing.provider}:<model>` to retry. "
"Or pick a different provider with `/model`."
)
else:
from deepagents_code.update_check import install_package_command
try:
install_cmd = install_package_command(missing.package)
except ValueError:
install_hint = f"install the `{missing.package}` package manually"
else:
install_hint = f"run `{install_cmd}`"
text += (
f"\n\nHint: {install_hint}, then run "
f"`/model {missing.provider}:<model>` "
"to retry. Or pick a different provider with `/model`."
)
async def _mount_failure() -> None:
# Drop any prior failure widget (re-entrant on retry-then-fail).
@@ -3122,6 +3139,136 @@ class DeepAgentsApp(App):
ErrorMessage(f"Update failed: {type(exc).__name__}: {exc}"),
)
async def _handle_install_command(self, command: str) -> None:
"""Handle the `/install <extra>` slash command.
Adds an optional extra (e.g. `quickjs`, `daytona`) to the installed
dcode tool by re-running `uv tool install -U 'deepagents-code[<extra>]'`.
Refuses unknown extras unless the user passes a `--force` token.
Args:
command: The full slash command line (e.g. `'/install quickjs'`
or `'/install foo --force'`).
"""
parts = command.split()
force = "--force" in parts[1:]
extras = [p for p in parts[1:] if not p.startswith("-")]
if not extras:
await self._mount_message(
AppMessage(
"Usage: /install <extra> [--force]\nExample: /install quickjs",
),
)
return
if len(extras) > 1:
await self._mount_message(
AppMessage(
"Only one extra may be installed per /install command. "
f"Got: {', '.join(extras)}",
),
)
return
extra = extras[0].lower()
await self._mount_message(UserMessage(command))
try:
from deepagents_code.config import _is_editable_install
from deepagents_code.extras_info import (
KNOWN_EXTRAS,
MODEL_PROVIDER_EXTRAS,
SANDBOX_EXTRAS,
)
from deepagents_code.update_check import (
create_update_log_path,
install_extra_command,
is_valid_extra_name,
perform_install_extra,
)
except ImportError as exc:
logger.warning("/install command import failed", exc_info=True)
await self._mount_message(
ErrorMessage(f"Install failed: {type(exc).__name__}: {exc}"),
)
return
if not is_valid_extra_name(extra):
await self._mount_message(
AppMessage(
"Invalid extra name. Extra names must be "
"alphanumeric with `-`, `_`, or `.` (PEP 508).",
),
)
return
if await asyncio.to_thread(_is_editable_install):
await self._mount_message(
AppMessage(
"Cannot install extras on editable installs. "
f"Run `uv sync --extra {extra}` from the "
"deepagents-code source directory instead.",
),
)
return
if extra not in KNOWN_EXTRAS and not force:
known = ", ".join(sorted(KNOWN_EXTRAS))
await self._mount_message(
AppMessage(
f"'{extra}' is not a known extra.\n"
f"Known extras: {known}\n\n"
f"This would run: `{install_extra_command(extra)}`\n"
f"Re-run with `--force` to install anyway: "
f"`/install {extra} --force`",
),
)
return
log_path = create_update_log_path()
await self._mount_message(
AppMessage(f"Installing extra '{extra}'..."),
)
manual_cmd = install_extra_command(extra)
try:
success, output = await perform_install_extra(extra, log_path=log_path)
except (OSError, asyncio.CancelledError) as exc:
logger.warning("/install command failed", exc_info=True)
await self._mount_message(
ErrorMessage(
f"Install failed: {type(exc).__name__}: {exc}\n"
f"Log: {log_path}\n"
f"Run manually: {manual_cmd}",
),
)
return
if not success:
# Tail the last 200 chars — uv resolver prints the resolved
# error at the end, not the beginning.
detail = f": {output[-200:]}" if output else ""
await self._mount_message(
ErrorMessage(
f"Install failed{detail}\n"
f"Log: {log_path}\n"
f"Run manually: {manual_cmd}",
),
)
return
# Model-provider and sandbox extras are imported by the langgraph
# server subprocess; `/restart` respawns that subprocess and picks
# them up without exiting the TUI. `quickjs` (and other
# STANDALONE_EXTRAS) are wired into the parent process at startup
# (`verify_interpreter_deps` gates `--interpreter`), so a full
# relaunch is required.
if extra in MODEL_PROVIDER_EXTRAS or extra in SANDBOX_EXTRAS:
next_step = "Run `/restart` to load it now, or relaunch dcode."
else:
next_step = "Exit and relaunch dcode to use the new dependencies."
await self._mount_message(
AppMessage(f"Installed extra '{extra}'. {next_step}"),
)
async def _handle_version_command(self) -> None:
"""Handle the `/version` slash command — show versions and update status.
@@ -3174,7 +3321,7 @@ class DeepAgentsApp(App):
except Exception:
logger.warning(
"Could not resolve upgrade command for /version; "
"falling back to generic pip hint",
"falling back to generic upgrade hint",
exc_info=True,
)
from deepagents_code.update_check import FALLBACK_UPGRADE_COMMAND
@@ -4972,7 +5119,9 @@ class DeepAgentsApp(App):
await self._mount_message(
AppMessage(
"The `langsmith` package is not installed. "
"Install it with `pip install langsmith` to enable `/trace`.",
"Install it with "
"`uv tool install -U deepagents-code --with langsmith` "
"to enable `/trace`.",
),
)
return
@@ -5070,7 +5219,8 @@ class DeepAgentsApp(App):
"/mcp, /model [--model-params JSON] [--default], "
"/notifications, /reload, /skill:<name>, /remember, "
"/skill-creator, /theme, /tokens, /threads, /trace, "
"/update, /auto-update, /changelog, /docs, /feedback, /help\n\n"
"/update, /auto-update, /install, /changelog, /docs, "
"/feedback, /help\n\n"
"Interactive Features:\n"
" Enter Submit your message\n"
f" {newline_shortcut():<15} Insert newline\n"
@@ -5180,6 +5330,8 @@ class DeepAgentsApp(App):
await self._handle_update_command()
elif cmd == "/auto-update":
await self._handle_auto_update_toggle()
elif cmd == "/install" or cmd.startswith("/install "):
await self._handle_install_command(command)
elif cmd == "/tokens":
await self._mount_message(UserMessage(command))
if self._context_tokens > 0:
@@ -183,6 +183,13 @@ COMMANDS: tuple[SlashCommand, ...] = (
bypass_tier=BypassTier.QUEUED,
hidden_keywords="upgrade",
),
SlashCommand(
name="/install",
description="Install an optional extra (e.g. quickjs, daytona, fireworks)",
bypass_tier=BypassTier.QUEUED,
hidden_keywords="extra extras add provider sandbox dependency",
argument_hint="<extra> [--force]",
),
SlashCommand(
name="/auto-update",
description="Toggle automatic updates on or off",
+21 -4
View File
@@ -2464,10 +2464,27 @@ def _create_model_via_init(
f"import for provider '{provider}': {e}"
)
else:
msg = (
f"Missing package for provider '{provider}'. "
f"Install: pip install {package}"
)
from deepagents_code.extras_info import extra_for_package
extra = extra_for_package(package)
if extra is not None:
msg = (
f"Missing package for provider '{provider}'. "
f"Install: /install {extra}"
)
else:
from deepagents_code.update_check import install_package_command
try:
install_cmd = install_package_command(package)
except ValueError:
install_hint = f"Install the '{package}' package manually"
else:
install_hint = f"Install with: {install_cmd}"
msg = (
f"Missing package for provider '{provider}'. "
f"{install_hint}, then retry with `/model`."
)
raise MissingProviderPackageError(
msg, provider=provider, package=package
) from e
+67 -2
View File
@@ -18,6 +18,7 @@ from importlib.metadata import (
)
from packaging.requirements import InvalidRequirement, Requirement
from packaging.utils import canonicalize_name
logger = logging.getLogger(__name__)
@@ -51,7 +52,7 @@ MODEL_PROVIDER_EXTRAS: frozenset[str] = frozenset(
"openrouter",
"perplexity",
"together",
"vertexai",
"vertex",
"xai",
}
)
@@ -71,6 +72,18 @@ These integrations layer onto the main agent (e.g. a JS REPL via
`all-sandboxes`.
"""
KNOWN_EXTRAS: frozenset[str] = (
MODEL_PROVIDER_EXTRAS | SANDBOX_EXTRAS | STANDALONE_EXTRAS
)
"""Union of all individually-installable extras.
Excludes the composite meta-extras (`all-providers`, `all-sandboxes`) since
those expand to other extras and don't add anything on their own.
Drift-protected by `test_model_config.TestProviderApiKeyEnv` and the
model-provider-drift checks; new extras must be added to the corresponding
category frozenset above.
"""
ExtrasStatus = dict[str, list[tuple[str, str]]]
"""Mapping from extra name to `(package, installed_version)` tuples.
@@ -202,6 +215,58 @@ def get_optional_dependency_status(
)
def extra_for_package(
package: str,
distribution_name: str = "deepagents-code",
) -> str | None:
"""Return the installable extra that declares a package.
Resolves recovery hints from the package that is actually missing
instead of guessing from a provider identifier. For example,
`langchain-google-vertexai` maps to the `vertex` extra even though the
provider id is `google_vertexai`.
Args:
package: Distribution package name to find in optional dependencies.
distribution_name: Name of the installed distribution to inspect.
Returns:
The known extra name that declares `package`, or `None` when the
package is not declared by an individually-installable extra,
or when the distribution's metadata could not be read (logged
at `warning` level — callers should treat both cases the same
since the right fallback in either is `install_package_command`).
"""
try:
dist = distribution(distribution_name)
except PackageNotFoundError:
logger.warning(
"Distribution %s not found; cannot resolve extra for package %s",
distribution_name,
package,
)
return None
own_name = canonicalize_name(distribution_name)
target = canonicalize_name(package)
for raw in dist.requires or []:
try:
req = Requirement(raw)
except InvalidRequirement:
logger.warning("Could not parse Requires-Dist entry: %s", raw)
continue
if canonicalize_name(req.name) != target:
continue
if canonicalize_name(req.name) == own_name:
continue
if not req.marker:
continue
extra = _extract_extra_name(str(req.marker))
if extra in KNOWN_EXTRAS:
return extra
return None
def verify_interpreter_deps() -> None:
"""Check that `langchain-quickjs` is installed for the `--interpreter` flag.
@@ -227,7 +292,7 @@ def verify_interpreter_deps() -> None:
if not found:
msg = (
"Missing dependencies for --interpreter. "
"Install with: pip install 'deepagents-code[quickjs]'"
"Install with: dcode --install quickjs"
)
raise ImportError(msg)
@@ -223,7 +223,8 @@ def _import_provider_module(
except ImportError as exc:
msg = (
f"The '{provider}' sandbox provider requires the '{package}' package. "
f"Install it with: pip install 'deepagents-code[{provider}]'"
f"Install it with: /install {provider} (in-app) or "
f"dcode --install {provider} (CLI)"
)
raise ImportError(msg) from exc
@@ -910,7 +911,8 @@ def verify_sandbox_deps(provider: str) -> None:
if not found:
msg = (
f"Missing dependencies for '{provider}' sandbox. "
f"Install with: pip install 'deepagents-code[{extra}]'"
f"Install with: /install {extra} (in-app) or "
f"dcode --install {extra} (CLI)"
)
raise ImportError(msg)
+120 -5
View File
@@ -201,10 +201,10 @@ def check_cli_dependencies() -> None:
print("\nThe following packages are required to use Deep Agents Code:") # noqa: T201 # App output for missing dependencies
for pkg in missing:
print(f" - {pkg}") # noqa: T201 # CLI output for missing dependencies
print("\nPlease install them with:") # noqa: T201 # CLI output for missing dependencies
print(" pip install deepagents[cli]") # noqa: T201 # CLI output for missing dependencies
print("\nOr install all dependencies:") # noqa: T201 # CLI output for missing dependencies
print(" pip install 'deepagents[cli]'") # noqa: T201 # CLI output for missing dependencies
print("\nReinstall dcode with the recommended installer:") # noqa: T201 # CLI output for missing dependencies
print(" curl -LsSf https://langch.in/dcode | bash") # noqa: T201 # CLI output for missing dependencies
print("\nOr install the tool directly via uv:") # noqa: T201 # CLI output for missing dependencies
print(" uv tool install -U deepagents-code") # noqa: T201 # CLI output for missing dependencies
sys.exit(1)
@@ -989,6 +989,16 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Toggle automatic updates on or off, then exit",
)
parser.add_argument(
"--install",
metavar="EXTRA",
help="Install an optional extra (e.g. quickjs, daytona, fireworks), then exit",
)
parser.add_argument(
"--yes",
action="store_true",
help="Skip interactive confirmation prompts (e.g., for --install)",
)
parser.add_argument(
"--acp",
action="store_true",
@@ -1780,7 +1790,8 @@ def cli_main() -> None:
except ImportError as exc:
msg = (
f"ACP dependencies not available: {exc}\n"
"Install with: pip install deepagents-acp\n"
"Install with: uv tool install -U deepagents-code "
"--with deepagents-acp\n"
)
sys.stderr.write(msg)
sys.stderr.flush()
@@ -1969,6 +1980,110 @@ def cli_main() -> None:
)
sys.exit(1)
# Handle --install <extra> flag (headless, no session)
if args.install:
from rich.markup import escape
from deepagents_code.config import _is_editable_install
from deepagents_code.extras_info import KNOWN_EXTRAS
from deepagents_code.update_check import (
create_update_log_path,
install_extra_command,
is_valid_extra_name,
perform_install_extra,
)
extra: str = args.install
log_path: Path | None = None
try:
if not is_valid_extra_name(extra):
# Defense in depth — the extra is interpolated into a
# shell command. Reject malformed names before any
# confirmation prompt, even with --yes.
console.print(
f"[bold red]Error:[/bold red] "
f"Invalid extra name '{escape(extra)}'. "
"Extra names must be alphanumeric with `-`, `_`, "
"or `.` (PEP 508).",
highlight=False,
)
sys.exit(2)
if _is_editable_install():
console.print(
"[bold yellow]Warning:[/bold yellow] "
"--install is not supported on editable installs. "
f"Run [cyan]uv sync --extra {extra}[/cyan] from the "
"deepagents-code source directory instead."
)
sys.exit(1)
if extra not in KNOWN_EXTRAS:
known = ", ".join(sorted(KNOWN_EXTRAS))
console.print(
f"[bold yellow]Warning:[/bold yellow] "
f"'{extra}' is not a known extra.\n"
f"Known extras: {known}",
highlight=False,
)
console.print(
f"This will run: [cyan]{install_extra_command(extra)}[/cyan]"
)
if not args.yes:
if not sys.stdin.isatty():
console.print(
"[bold red]Error:[/bold red] "
"Refusing unknown extra in non-interactive "
"mode. Pass --yes to override."
)
sys.exit(2)
reply = input("Continue anyway? [y/N] ").strip().lower()
if reply not in {"y", "yes"}:
console.print("Aborted.", style="dim")
sys.exit(1)
console.print(f"Installing extra '{extra}'...")
log_path = create_update_log_path()
console.print(
f"Install log: {log_path}\nTail progress: tail -f {log_path}",
style="dim",
highlight=False,
markup=False,
)
success, output = asyncio.run(
perform_install_extra(extra, log_path=log_path)
)
if success:
console.print(f"[green]Installed extra '{extra}'.[/green]")
sys.exit(0)
# Tail the last 200 chars — uv resolver prints the resolved
# error at the end, not the beginning.
detail = f": {output[-200:]}" if output else ""
console.print(
f"[bold red]Install failed[/bold red]{escape(detail)}\n"
f"Log: {log_path}\n"
f"Run manually: [cyan]{install_extra_command(extra)}[/cyan]",
markup=True,
highlight=False,
)
sys.exit(1)
except KeyboardInterrupt:
console.print("\nAborted.", style="dim")
sys.exit(130)
except Exception as exc:
logger.warning("--install failed", exc_info=True)
log_line = f"\nLog: {log_path}" if log_path else ""
console.print(
f"[bold red]Error:[/bold red] "
f"{type(exc).__name__}: {escape(str(exc))}"
f"{escape(log_line)}\n"
"Run manually: [cyan]"
f"uv tool install -U 'deepagents-code[{escape(extra)}]'"
"[/cyan]",
markup=True,
highlight=False,
)
sys.exit(1)
# Handle --auto-update flag (headless toggle: reads current state
# and inverts it, no session)
if args.auto_update:
+3 -3
View File
@@ -191,9 +191,9 @@ class MissingProviderPackageError(ModelConfigError):
Subclasses `ModelConfigError` so existing `except ModelConfigError` blocks
keep working. Carries the `provider` name and the `package` to install so
callers can render targeted recovery hints (e.g., suggest
`pip install langchain-fireworks` or the `/model` slash command) without
string-matching on the formatted exception message.
callers can render targeted recovery hints (e.g., suggest `/install fireworks`
or the `/model` slash command) without string-matching on the formatted
exception message.
"""
def __init__(self, message: str, *, provider: str, package: str) -> None:
+4
View File
@@ -167,6 +167,10 @@ def show_help() -> None:
console.print(
" --auto-update Toggle automatic updates on or off, then exit"
)
console.print(
" --install EXTRA Install an optional extra (e.g. quickjs)"
)
console.print(" --yes Skip --install confirmation prompts")
console.print(" --acp Run as an ACP server over stdio")
console.print(" -v, --version Show dcode and SDK versions")
console.print(" -h, --help Show this help message and exit")
+219 -38
View File
@@ -16,6 +16,7 @@ import json
import logging
import operator
import os
import re
import shutil
import sys
import time
@@ -67,28 +68,33 @@ INSTALLED_AGE_NOTICE_DAYS = 7
_SDK_RELEASE_TIMES_KEY = "sdk_release_times"
"""`CACHE_FILE` key for cached SDK upload timestamps, keyed by version string."""
InstallMethod = Literal["uv", "pip", "brew", "unknown"]
InstallMethod = Literal["uv", "brew", "other", "unknown"]
FALLBACK_UPGRADE_COMMAND = "pip install --upgrade deepagents-code"
FALLBACK_UPGRADE_COMMAND = "uv tool upgrade deepagents-code"
"""Generic upgrade hint used when install-method detection fails.
Callers that surface an upgrade command in user-facing text should prefer
`upgrade_command()`; this constant exists so those callers have something
to render when detection raises unexpectedly.
to render when detection raises unexpectedly. The documented install path
is `uv tool install` (see `scripts/install.sh`), so the uv command is the
right display fallback. Execution paths still refuse unrecognized installs
instead of updating a separate environment.
"""
_UPGRADE_COMMANDS: dict[InstallMethod, str] = {
"uv": "uv tool upgrade deepagents-code",
"brew": "brew upgrade deepagents-code",
"pip": FALLBACK_UPGRADE_COMMAND,
}
"""Upgrade commands keyed by install method.
`perform_upgrade` runs only the command matching the detected install method;
no fallback chain.
no fallback chain. Unknown non-editable installs are refused rather than
upgraded with a different package manager, because that can update a separate
environment from the one currently providing `dcode`.
"""
_UPGRADE_TIMEOUT = 120 # seconds
"""Wall-clock cap for `perform_upgrade` and `perform_install_extra`."""
UPDATE_LOG_DIR: Path = DEFAULT_STATE_DIR / "update_logs"
"""Directory for persisted update command logs."""
@@ -194,7 +200,7 @@ def get_latest_version(
except ImportError:
logger.warning(
"requests package not installed — update checks disabled. "
"Install with: pip install requests"
"Install with: uv tool install -U deepagents-code --with requests"
)
return cached_version
@@ -646,7 +652,7 @@ def detect_install_method() -> InstallMethod:
Checks `sys.prefix` against known paths for uv and Homebrew.
Returns:
The detected install method: `'uv'`, `'brew'`, `'pip'`, or `'unknown'`
The detected install method: `'uv'`, `'brew'`, `'other'`, or `'unknown'`
(editable/dev installs).
"""
from deepagents_code.config import _is_editable_install
@@ -664,13 +670,13 @@ def detect_install_method() -> InstallMethod:
# Editable / dev installs — don't auto-upgrade
if _is_editable_install():
return "unknown"
return "pip"
return "other"
def upgrade_command(method: InstallMethod | None = None) -> str:
"""Return the shell command to upgrade `deepagents-code`.
Falls back to the pip command for unrecognized install methods.
Falls back to the documented uv command for display-only guidance.
Args:
method: Install method override.
@@ -679,7 +685,7 @@ def upgrade_command(method: InstallMethod | None = None) -> str:
"""
if method is None:
method = detect_install_method()
return _UPGRADE_COMMANDS.get(method, _UPGRADE_COMMANDS["pip"])
return _UPGRADE_COMMANDS.get(method, FALLBACK_UPGRADE_COMMAND)
def cleanup_update_logs(
@@ -750,35 +756,32 @@ async def _read_stream(
await _emit_progress(progress, line)
async def perform_upgrade(
async def _run_install_subprocess(
cmd: str,
*,
progress: UpgradeProgressCallback | None = None,
log_path: Path | None = None,
progress: UpgradeProgressCallback | None,
log_path: Path | None,
) -> tuple[bool, str]:
"""Attempt to upgrade `deepagents-code` using the detected install method.
"""Run a shell command, streaming stdout/stderr to *progress* and a log file.
Only tries the detected method — does not fall back to other package
managers to avoid cross-environment contamination.
Shared subprocess plumbing for `perform_upgrade` and
`perform_install_extra`. Returns `(success, combined_output)` where
*combined_output* is the concatenated stdout+stderr, stripped.
On timeout or `OSError`, the process is killed and a synthetic error
line is emitted both to the log and via *progress*. The wall-clock cap
is `_UPGRADE_TIMEOUT`.
Args:
cmd: Shell command to execute.
progress: Optional callback invoked for each output line.
log_path: Optional path to persist command output.
log_path: Optional path to persist command output. Falls back to a
fresh `create_update_log_path()` when `None`.
Returns:
`(success, output)` — *output* is the combined stdout/stderr.
`(success, output)` — *success* is `True` iff the subprocess exited 0.
"""
method = detect_install_method()
if method == "unknown":
return False, "Editable install detected — skipping auto-update."
cmd = _UPGRADE_COMMANDS.get(method)
if cmd is None:
return False, f"No upgrade command for install method: {method}"
# Skip brew if binary not on PATH
if method == "brew" and not shutil.which("brew"):
return False, "brew not found on PATH."
timeout = _UPGRADE_TIMEOUT
if log_path is None:
log_path = create_update_log_path()
@@ -791,7 +794,12 @@ async def perform_upgrade(
log_file.write(f"$ {cmd}\n")
log_file.flush()
except OSError:
logger.debug("Could not create update log at %s", log_path, exc_info=True)
logger.warning(
"Could not create install log at %s; subprocess output will not be "
"persisted to disk",
log_path,
exc_info=True,
)
log_file = None
try:
@@ -817,13 +825,13 @@ async def perform_upgrade(
),
proc.wait(),
),
timeout=_UPGRADE_TIMEOUT,
timeout=timeout,
)
except TimeoutError:
if proc is not None:
proc.kill()
await proc.wait()
msg = f"Upgrade command timed out after {_UPGRADE_TIMEOUT}s: {cmd}"
msg = f"Command timed out after {timeout}s: {cmd}"
if log_file is not None:
with suppress(OSError):
log_file.write(f"{msg}\n")
@@ -831,12 +839,12 @@ async def perform_upgrade(
await _emit_progress(progress, msg)
logger.warning(msg)
return False, msg
except OSError:
except OSError as exc:
if log_file is not None:
with suppress(OSError):
log_file.close()
logger.warning("Failed to execute upgrade command: %s", cmd, exc_info=True)
return False, f"Failed to execute: {cmd}"
logger.warning("Failed to execute command: %s", cmd, exc_info=True)
return False, f"Failed to execute: {cmd}\n{type(exc).__name__}: {exc}"
if log_file is not None:
with suppress(OSError):
@@ -845,14 +853,187 @@ async def perform_upgrade(
if proc.returncode == 0:
return True, output
logger.warning(
"Upgrade via %s exited with code %d: %s",
method,
"Command exited with code %d: %s\n%s",
proc.returncode,
cmd,
output,
)
return False, output
async def perform_upgrade(
*,
progress: UpgradeProgressCallback | None = None,
log_path: Path | None = None,
) -> tuple[bool, str]:
"""Attempt to upgrade `deepagents-code` using the detected install method.
Only tries the detected method — does not fall back to other package
managers to avoid cross-environment contamination.
Args:
progress: Optional callback invoked for each output line.
log_path: Optional path to persist command output.
Returns:
`(success, output)` — *output* is the combined stdout/stderr.
"""
method = detect_install_method()
if method == "unknown":
return False, "Editable install detected — skipping auto-update."
if method == "other":
return False, (
"Unsupported install method detected — cannot auto-update without "
"knowing which environment provides `dcode`. Reinstall with "
"`uv tool install -U deepagents-code` or upgrade with the package "
"manager originally used for this install."
)
cmd = _UPGRADE_COMMANDS.get(method)
if cmd is None:
return False, f"No upgrade command for install method: {method}"
# Skip brew if binary not on PATH
if method == "brew" and not shutil.which("brew"):
return False, "brew not found on PATH."
return await _run_install_subprocess(cmd, progress=progress, log_path=log_path)
_EXTRA_NAME_RE = re.compile(r"^[A-Za-z0-9](?:[-_.A-Za-z0-9]*[A-Za-z0-9])?$")
"""Conservative package-extra name pattern used before shell command display."""
_PACKAGE_NAME_RE = re.compile(r"^[A-Za-z0-9](?:[-_.A-Za-z0-9]*[A-Za-z0-9])?$")
"""Conservative package name pattern used before shell command display."""
def is_valid_extra_name(extra: str) -> bool:
"""Return whether `extra` is safe to embed in package-extra syntax.
Args:
extra: Candidate extra name from CLI or slash-command input.
Returns:
`True` when the value is a conservative PEP 508-style extra name.
"""
return bool(_EXTRA_NAME_RE.fullmatch(extra))
def install_package_command(package: str) -> str:
"""Return the shell command that adds a package to the dcode tool env.
Args:
package: Package name to install into the existing tool environment.
Returns:
Shell command string suitable for display in error messages.
Raises:
ValueError: If `package` is not a conservative PEP 508-style package
name.
"""
if not _PACKAGE_NAME_RE.fullmatch(package):
msg = (
f"Invalid package name {package!r}: must match PEP 508 "
f"({_PACKAGE_NAME_RE.pattern})"
)
raise ValueError(msg)
return f"uv tool install -U deepagents-code --with {package}"
def install_extra_command(extra: str) -> str:
"""Return the shell command that adds `extra` to the installed dcode tool.
The documented install path is `uv tool install` (see
`scripts/install.sh`), so the only correct way to add an extra to an
existing dcode install is to reinstall the tool with the extra
specified. Single-quoting the bracket form keeps zsh from globbing it.
Args:
extra: The extra name (e.g. `'quickjs'`, `'daytona'`, `'fireworks'`).
Validated internally against PEP 508 grammar before interpolation
into the shell command.
Returns:
Shell command string suitable for display in error messages and
for execution via `perform_install_extra`.
Raises:
ValueError: If `extra` fails PEP 508 validation. Prevents shell
injection via crafted bracket-escape sequences.
"""
if not is_valid_extra_name(extra):
msg = (
f"Invalid extra name {extra!r}: must match PEP 508 "
f"({_EXTRA_NAME_RE.pattern})"
)
raise ValueError(msg)
return f"uv tool install -U 'deepagents-code[{extra}]'"
async def perform_install_extra(
extra: str,
*,
progress: UpgradeProgressCallback | None = None,
log_path: Path | None = None,
) -> tuple[bool, str]:
"""Add `extra` to the installed dcode tool environment.
Runs `uv tool install -U 'deepagents-code[<extra>]'`. Editable installs
are refused — the caller should instead `uv sync --extra <extra>` from
the package directory, which we cannot do without knowing the source
location.
Args:
extra: The extra name to install. Must satisfy `is_valid_extra_name`;
invalid names are rejected without invoking uv (defense in depth
against shell injection via the `--force`/`--yes` bypass paths).
progress: Optional callback invoked for each output line.
log_path: Optional path to persist command output.
Returns:
`(success, output)` — *output* is the combined stdout/stderr, or an
explanatory error message when the install method is unsupported
or `extra` is malformed.
"""
if not is_valid_extra_name(extra):
return False, (
f"Invalid extra name {extra!r}: must match {_EXTRA_NAME_RE.pattern}"
)
method = detect_install_method()
if method == "unknown":
return False, (
"Editable install detected — cannot add extras automatically. "
f"Run `uv sync --extra {extra}` from the deepagents-code source "
"directory instead."
)
if method == "brew":
# Homebrew formula doesn't expose extras; uv tool install is the
# right escape hatch but would conflict with the brew-managed binary.
return False, (
"Homebrew install detected — extras are not supported via brew. "
"Reinstall with `uv tool install -U 'deepagents-code["
f"{extra}]'` to switch to a uv-managed tool install with extras."
)
if method == "other":
return False, (
"Unsupported install method detected — cannot add extras without "
"knowing which environment provides `dcode`. Reinstall with "
f"`uv tool install -U 'deepagents-code[{extra}]'` to switch to a "
"uv-managed tool install with extras."
)
if not shutil.which("uv"):
return False, (
"`uv` not found on PATH. Reinstall dcode following the docs, or "
"install uv (https://docs.astral.sh/uv/) so extras can be added."
)
cmd = install_extra_command(extra)
return await _run_install_subprocess(cmd, progress=progress, log_path=log_path)
# ---------------------------------------------------------------------------
# Config helpers
# ---------------------------------------------------------------------------
+1 -1
View File
@@ -509,7 +509,7 @@ class AuthManagerScreen(ModalScreen[None]):
return Content.assemble(
"Lists installed providers and any you've configured in "
"~/.deepagents/config.toml. Install more via "
"`pip install deepagents-code[<provider>]`. ",
"`/install <provider>`. ",
("Docs", link_style),
)
@@ -261,7 +261,7 @@ class LaunchDependenciesScreen(ModalScreen[bool | None]):
# single explanatory line instead of "none detected" twice.
yield Static(
"Could not read installed dependency metadata. Reinstall "
"with `pip install deepagents-code[<extra>]` to populate.",
"with `/install <extra>` to populate.",
classes="launch-dependencies-section",
)
yield Static(
@@ -49,6 +49,7 @@ _TIPS: dict[str, int] = {
"Press ctrl+u to delete to the start of the line in the chat input": 1,
"Use /skill:<name> to invoke a skill directly": 1,
"Type /update to check for and install updates": 1,
"Use /install <extra> to add optional dependencies (e.g. /install quickjs)": 1,
"Use /theme to customize the TUI's colors": 1,
"In /theme, press N to toggle labels/keys, T to set for the current terminal": 1,
"Use /skill-creator to build reusable agent skills": 1,
+2 -2
View File
@@ -100,10 +100,10 @@ openai = ["langchain-openai>=1.2.1,<2.0.0"]
openrouter = ["langchain-openrouter>=0.2.3,<2.0.0"]
perplexity = ["langchain-perplexity>=1.2.0,<2.0.0"]
together = ["langchain-together>=0.4.0,<2.0.0"]
vertexai = ["langchain-google-vertexai>=3.2.3,<4.0.0"]
vertex = ["langchain-google-vertexai>=3.2.3,<4.0.0"]
xai = ["langchain-xai>=1.2.2,<2.0.0"]
all-providers = [
"deepagents-code[anthropic,baseten,bedrock,cohere,deepseek,fireworks,google-genai,groq,huggingface,ibm,litellm,mistralai,nvidia,ollama,openai,openrouter,perplexity,together,vertexai,xai]",
"deepagents-code[anthropic,baseten,bedrock,cohere,deepseek,fireworks,google-genai,groq,huggingface,ibm,litellm,mistralai,nvidia,ollama,openai,openrouter,perplexity,together,vertex,xai]",
]
# Sandbox providers
+82 -8
View File
@@ -5370,8 +5370,7 @@ class TestDeferredActions:
app._connecting = True
error = MissingProviderPackageError(
"Missing package for provider 'fireworks'. "
"Install: pip install langchain-fireworks",
"Missing package for provider 'fireworks'. Install: /install fireworks",
provider="fireworks",
package="langchain-fireworks",
)
@@ -5391,14 +5390,87 @@ class TestDeferredActions:
widget = app._startup_failure_widget
assert isinstance(widget, ErrorMessage)
rendered = str(widget._content)
assert "pip install langchain-fireworks" in rendered
# `fireworks` is a known extra → /install hint, not the raw uv form.
assert "/install fireworks" in rendered
assert "/model fireworks:<model>" in rendered
async def test_server_failure_missing_vertexai_package_uses_declared_extra(
self,
) -> None:
"""Startup hint should resolve extras from missing packages."""
from deepagents_code.model_config import MissingProviderPackageError
from deepagents_code.widgets.messages import ErrorMessage
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._server_kwargs = {"model_name": "google_vertexai:fake"}
app._connecting = True
error = MissingProviderPackageError(
"Missing package for provider 'google_vertexai'. "
"Install: /install vertex",
provider="google_vertexai",
package="langchain-google-vertexai",
)
with patch(
"deepagents_code.extras_info.extra_for_package",
return_value="vertex",
) as mock_extra_for_package:
app.on_deep_agents_app_server_start_failed(
DeepAgentsApp.ServerStartFailed(error=error)
)
await pilot.pause()
mock_extra_for_package.assert_called_once_with("langchain-google-vertexai")
widget = app._startup_failure_widget
assert isinstance(widget, ErrorMessage)
rendered = str(widget._content)
assert "/install vertex" in rendered
assert "/install google-vertexai" not in rendered
assert "/model google_vertexai:<model>" in rendered
async def test_server_failure_missing_unknown_package_shows_uv_command(
self,
) -> None:
"""Manual fallback should include the default uv tool command."""
from deepagents_code.model_config import MissingProviderPackageError
from deepagents_code.widgets.messages import ErrorMessage
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
app._server_kwargs = {"model_name": "custom_provider:fake"}
app._connecting = True
error = MissingProviderPackageError(
"Missing package for provider 'custom_provider'.",
provider="custom_provider",
package="langchain-custom_provider",
)
with patch(
"deepagents_code.extras_info.extra_for_package",
return_value=None,
):
app.on_deep_agents_app_server_start_failed(
DeepAgentsApp.ServerStartFailed(error=error)
)
await pilot.pause()
widget = app._startup_failure_widget
assert isinstance(widget, ErrorMessage)
rendered = str(widget._content)
assert (
"uv tool install -U deepagents-code --with langchain-custom_provider"
in rendered
)
assert "/model custom_provider:<model>" in rendered
async def test_retry_startup_clears_missing_package_slot(self) -> None:
"""`_retry_startup_with_model` must clear the package recovery slot.
Mirrors the credentials-slot reset directly above it. A regression
that drops the reset would leave a stale `pip install` hint visible
that drops the reset would leave a stale `/install` hint visible
after a successful retry, or render the wrong hint on the next failure.
"""
from deepagents_code.model_config import (
@@ -6394,7 +6466,9 @@ def _update_entry(latest: str = "2.0.0") -> PendingNotification:
NotificationAction(ActionId.SKIP_ONCE, "Remind me next launch"),
NotificationAction(ActionId.SKIP_VERSION, "Skip this version"),
),
payload=UpdateAvailablePayload(latest=latest, upgrade_cmd="pip install"),
payload=UpdateAvailablePayload(
latest=latest, upgrade_cmd="uv tool upgrade deepagents-code"
),
)
@@ -6407,7 +6481,7 @@ def test_build_update_notification_uses_release_and_installed_age_copy() -> None
cli_version="1.0.0",
release_age=" (released 3d ago)",
installed_age=" (8 days old)",
upgrade_cmd="pip install",
upgrade_cmd="uv tool upgrade deepagents-code",
)
assert notification.body == (
@@ -7289,7 +7363,7 @@ class TestNotificationCenterIntegration:
),
patch(
"deepagents_code.update_check.upgrade_command",
return_value="pip install -U deepagents-code",
return_value="uv tool upgrade deepagents-code",
),
):
async with app.run_test() as pilot:
@@ -7338,7 +7412,7 @@ class TestNotificationCenterIntegration:
),
patch(
"deepagents_code.update_check.upgrade_command",
return_value="pip install -U deepagents-code",
return_value="uv tool upgrade deepagents-code",
),
):
async with app.run_test() as pilot:
+30 -1
View File
@@ -2516,6 +2516,32 @@ class TestCreateModelViaInitImportError:
# Subclasses ModelConfigError so existing handlers keep working.
assert isinstance(exc_info.value, ModelConfigError)
@patch("langchain.chat_models.init_chat_model")
def test_missing_vertexai_package_uses_declared_extra(
self, mock_init: Mock
) -> None:
"""Vertex AI provider id does not match its optional extra name."""
from deepagents_code.model_config import MissingProviderPackageError
mock_init.side_effect = ImportError(
"No module named 'langchain_google_vertexai'"
)
with (
patch("importlib.util.find_spec", return_value=None),
patch(
"deepagents_code.extras_info.extra_for_package",
return_value="vertex",
) as mock_extra_for_package,
pytest.raises(
MissingProviderPackageError,
match=r"Install: /install vertex",
) as exc_info,
):
_create_model_via_init("claude-sonnet-4-5", "google_vertexai", {})
mock_extra_for_package.assert_called_once_with("langchain-google-vertexai")
assert exc_info.value.provider == "google_vertexai"
assert exc_info.value.package == "langchain-google-vertexai"
@patch("langchain.chat_models.init_chat_model")
def test_installed_but_broken_import(self, mock_init: Mock) -> None:
"""Shows real error when package is installed but import fails internally."""
@@ -2552,7 +2578,10 @@ class TestCreateModelViaInitImportError:
patch("importlib.util.find_spec", return_value=None),
pytest.raises(
ModelConfigError,
match=r"pip install langchain-custom_provider",
match=(
"Install with: uv tool install -U deepagents-code "
"--with langchain-custom_provider"
),
),
):
_create_model_via_init("some-model", "custom_provider", {})
+36 -1
View File
@@ -9,9 +9,11 @@ import pytest
from deepagents_code.extras_info import (
_COMPOSITE_EXTRAS,
KNOWN_EXTRAS,
MODEL_PROVIDER_EXTRAS,
SANDBOX_EXTRAS,
STANDALONE_EXTRAS,
extra_for_package,
format_extras_status,
format_extras_status_plain,
get_extras_status,
@@ -135,6 +137,28 @@ def test_skips_entries_without_extra_marker() -> None:
assert extras == {"foo": [("gated-pkg", "1.2.3")]}
def test_extra_for_package_returns_declaring_known_extra() -> None:
"""Package lookup should use declared extras instead of provider-name guesses."""
mock_dist = MagicMock()
mock_dist.requires = [
"langchain-google-vertexai>=3.2.3,<4.0.0 ; extra == 'vertex'",
"deepagents-code[anthropic,baseten] ; extra == 'all-providers'",
]
with patch("deepagents_code.extras_info.distribution", return_value=mock_dist):
assert extra_for_package("langchain-google-vertexai") == "vertex"
def test_extra_for_package_returns_none_for_unknown_package() -> None:
mock_dist = MagicMock()
mock_dist.requires = [
"langchain-google-vertexai>=3.2.3,<4.0.0 ; extra == 'vertex'",
]
with patch("deepagents_code.extras_info.distribution", return_value=mock_dist):
assert extra_for_package("not-declared") is None
def test_skips_composite_self_referencing_extras() -> None:
mock_dist = MagicMock()
mock_dist.requires = [
@@ -223,6 +247,17 @@ def test_extras_taxonomy_covers_pyproject() -> None:
)
def test_known_extras_is_union_of_categories() -> None:
"""`KNOWN_EXTRAS` must be the union of the three category frozensets.
`dcode --install <extra>` and `/install <extra>` consult `KNOWN_EXTRAS`
to decide whether to prompt for confirmation on unknown values, so this
set has to stay aligned with the taxonomy or callers will see spurious
prompts for real extras.
"""
assert KNOWN_EXTRAS == (MODEL_PROVIDER_EXTRAS | SANDBOX_EXTRAS | STANDALONE_EXTRAS)
def test_extras_categories_are_disjoint() -> None:
"""An extra can only be classified in one taxonomy set."""
pairs = (
@@ -239,7 +274,7 @@ def test_verify_interpreter_deps_raises_when_module_missing() -> None:
patch(
"deepagents_code.extras_info.importlib.util.find_spec", return_value=None
),
pytest.raises(ImportError, match="deepagents-code\\[quickjs\\]"),
pytest.raises(ImportError, match="dcode --install quickjs"),
):
verify_interpreter_deps()
@@ -0,0 +1,242 @@
"""Tests for the `/install <extra>` slash command and `--install` flag handler.
The CLI-flag side is covered by `test_main_args.TestInstallExtraSubcommand`;
this module focuses on the in-app slash dispatch in `DeepAgentsApp`.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
from deepagents_code.app import DeepAgentsApp
from deepagents_code.widgets.messages import AppMessage, ErrorMessage
async def test_install_slash_usage_when_no_extra() -> None:
"""`/install` with no argument prints a usage hint, no install attempt."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
) as perform_mock:
await app._handle_command("/install")
await pilot.pause()
perform_mock.assert_not_awaited()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
assert any("Usage: /install" in str(m._content) for m in app_msgs)
async def test_install_slash_known_extra_runs() -> None:
"""A known extra invokes `perform_install_extra`."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(True, ""),
) as perform_mock,
):
await app._handle_command("/install quickjs")
await pilot.pause()
perform_mock.assert_awaited_once()
async def test_install_slash_provider_extra_recommends_restart_slash() -> None:
"""Provider extras advertise `/restart`, not a full relaunch.
The langgraph subprocess is what imports model-provider packages, so
respawning that subprocess via `/restart` picks them up without exiting.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(True, ""),
),
):
await app._handle_command("/install fireworks")
await pilot.pause()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
success = next(
m for m in app_msgs if "Installed extra 'fireworks'" in str(m._content)
)
assert "/restart" in str(success._content)
async def test_install_slash_standalone_extra_recommends_full_relaunch() -> None:
"""Standalone extras must require a full relaunch, not `/restart`.
`quickjs` and other `STANDALONE_EXTRAS` are wired into the TUI parent
at startup via `verify_interpreter_deps`, so a subprocess respawn
won't pick them up — the user has to exit and re-run dcode.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(True, ""),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
success = next(
m for m in app_msgs if "Installed extra 'quickjs'" in str(m._content)
)
rendered = str(success._content)
assert "/restart" not in rendered
assert "relaunch dcode" in rendered
async def test_install_slash_unknown_extra_requires_force() -> None:
"""Unknown extras without `--force` must not call `perform_install_extra`."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
) as perform_mock,
):
await app._handle_command("/install not-a-real-extra")
await pilot.pause()
perform_mock.assert_not_awaited()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
assert any("not a known extra" in str(m._content) for m in app_msgs)
async def test_install_slash_unknown_extra_with_force_runs() -> None:
"""`--force` bypasses the unknown-extra confirmation."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(True, ""),
) as perform_mock,
):
await app._handle_command("/install not-a-real-extra --force")
await pilot.pause()
perform_mock.assert_awaited_once()
async def test_install_slash_invalid_extra_refuses_even_with_force() -> None:
"""Malformed extras must not reach command construction."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
) as perform_mock,
):
await app._handle_command("/install quickjs'];touch --force")
await pilot.pause()
perform_mock.assert_not_awaited()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
assert any("Invalid extra name" in str(m._content) for m in app_msgs)
async def test_install_slash_failure_surfaces_log_path_and_manual_cmd() -> None:
"""A failed install renders as `ErrorMessage` with log path + manual cmd.
The success-styling regression: a previous version mounted `AppMessage`
on failure, which made it visually indistinguishable from the
"Installing extra..." status line. Failures must use `ErrorMessage`.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(False, "resolver: conflict"),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
error_msgs = [str(m._content) for m in app.query(ErrorMessage)]
joined = "\n".join(error_msgs)
assert "Install failed" in joined
assert "resolver: conflict" in joined
assert "/tmp/deepagents-install.log" in joined
assert "uv tool install -U 'deepagents-code[quickjs]'" in joined
async def test_install_slash_exception_surfaces_log_path_and_manual_cmd() -> None:
"""When `perform_install_extra` raises, surface log path + manual cmd."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
side_effect=OSError("disk full"),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
error_msgs = [str(m._content) for m in app.query(ErrorMessage)]
joined = "\n".join(error_msgs)
assert "OSError" in joined
assert "disk full" in joined
assert "/tmp/deepagents-install.log" in joined
assert "uv tool install -U 'deepagents-code[quickjs]'" in joined
async def test_install_slash_editable_install_refuses() -> None:
"""Editable installs must not invoke `perform_install_extra` from the TUI.
Mirrors the editable-install guard for `/update` — running `uv tool
install` on a dev checkout would clobber the editable install.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=True),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
) as perform_mock,
):
await app._handle_command("/install quickjs")
await pilot.pause()
perform_mock.assert_not_awaited()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
assert any(
"Cannot install extras on editable installs" in str(m._content)
for m in app_msgs
)
+197 -3
View File
@@ -1100,7 +1100,7 @@ class TestUpdateSubcommand:
"""Control-flow tests for `deepagents update` and `--update`.
Each branch has a destructive or user-visible failure mode (editable
install would have pip clobber a dev checkout; PyPI-unreachable must
install would clobber a dev checkout; PyPI-unreachable must
not be confused with up-to-date). These tests pin the dispatch order.
"""
@@ -1145,8 +1145,8 @@ class TestUpdateSubcommand:
def test_editable_install_skips_upgrade(self) -> None:
"""Editable install exits 0 without calling `is_update_available`/upgrade.
A regression here would run `pip install --upgrade` on an editable
checkout and overwrite the dev install.
A regression here would run `uv tool upgrade deepagents-code` on an
editable checkout and clobber the dev install with a PyPI copy.
"""
code, is_update_mock, perform_upgrade_mock = self._run_update(
editable=True,
@@ -1202,3 +1202,197 @@ class TestUpdateSubcommand:
)
assert code == 0
perform_upgrade_mock.assert_awaited_once()
class TestInstallExtraSubcommand:
"""Control-flow tests for `dcode --install <extra>`."""
@staticmethod
def _run_install(
extra: str,
*,
editable: bool = False,
yes: bool = False,
interactive: bool = False,
perform_return: tuple[bool, str] = (True, ""),
) -> tuple[int, MagicMock]:
"""Invoke `cli_main()` with `--install`; return exit code + mock."""
from deepagents_code.main import cli_main
argv = ["deepagents", "--install", extra]
if yes:
argv.append("--yes")
mock_stdin = MagicMock()
mock_stdin.isatty.return_value = interactive
with (
patch.object(sys, "argv", argv),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_cli_dependencies"),
patch("deepagents_code.config._is_editable_install", return_value=editable),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=perform_return,
) as perform_mock,
patch("builtins.input", return_value="n"),
pytest.raises(SystemExit) as exc_info,
):
cli_main()
return int(exc_info.value.code or 0), perform_mock
def test_known_extra_runs_install(self) -> None:
"""A known extra invokes `perform_install_extra` and exits 0."""
code, perform_mock = self._run_install("quickjs")
assert code == 0
perform_mock.assert_awaited_once()
def test_editable_install_refuses(self) -> None:
"""Editable install short-circuits with a `uv sync` hint, exit 1."""
code, perform_mock = self._run_install("quickjs", editable=True)
assert code == 1
perform_mock.assert_not_awaited()
def test_unknown_extra_non_interactive_refuses(self) -> None:
"""Non-TTY stdin + unknown extra + no --yes must exit 2 (refusal)."""
code, perform_mock = self._run_install("not-a-real-extra", interactive=False)
assert code == 2
perform_mock.assert_not_awaited()
def test_invalid_extra_refuses_even_with_yes(self) -> None:
"""Malformed extras must never reach the installer command path."""
code, perform_mock = self._run_install(
"quickjs']; echo nope; '",
yes=True,
interactive=False,
)
assert code == 2
perform_mock.assert_not_awaited()
def test_unknown_extra_with_yes_runs(self) -> None:
"""`--yes` bypasses the unknown-extra confirmation."""
code, perform_mock = self._run_install(
"not-a-real-extra", yes=True, interactive=False
)
assert code == 0
perform_mock.assert_awaited_once()
@staticmethod
def _run_install_capture(
extra: str,
*,
editable: bool = False,
yes: bool = False,
interactive: bool = False,
perform_return: tuple[bool, str] = (True, ""),
perform_side_effect: BaseException | None = None,
input_reply: str = "n",
) -> tuple[int, MagicMock, MagicMock]:
"""Invoke `cli_main()` with `--install` and capture console output.
Returns:
`(exit_code, perform_mock, console_mock)` — *console_mock* is a
`MagicMock` substituted for `deepagents_code.main.console`,
so assertions can run against the recorded `.print(...)` calls.
"""
from deepagents_code.main import cli_main
argv = ["deepagents", "--install", extra]
if yes:
argv.append("--yes")
mock_stdin = MagicMock()
mock_stdin.isatty.return_value = interactive
console_mock = MagicMock()
perform_mock = AsyncMock()
if perform_side_effect is not None:
perform_mock.side_effect = perform_side_effect
else:
perform_mock.return_value = perform_return
with (
patch.object(sys, "argv", argv),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_cli_dependencies"),
# `cli_main` resolves `console` via a lazy `__getattr__` on
# `deepagents_code.config`, so patch with `create=True` to
# install the mock before the import line runs.
patch("deepagents_code.config.console", console_mock, create=True),
patch("deepagents_code.config._is_editable_install", return_value=editable),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value=Path("/tmp/deepagents-install.log"),
),
patch(
"deepagents_code.update_check.perform_install_extra",
perform_mock,
),
patch("builtins.input", return_value=input_reply),
pytest.raises(SystemExit) as exc_info,
):
cli_main()
return int(exc_info.value.code or 0), perform_mock, console_mock
@staticmethod
def _printed_text(console_mock: MagicMock) -> str:
"""Return the concatenated positional args of every `.print()` call."""
chunks: list[str] = []
for call in console_mock.print.call_args_list:
chunks.extend(str(arg) for arg in call.args)
return "\n".join(chunks)
def test_success_renders_installed_message(self) -> None:
"""Successful install prints a green confirmation and exits 0."""
code, _perform, console_mock = self._run_install_capture("quickjs")
assert code == 0
text = self._printed_text(console_mock)
assert "Installed extra 'quickjs'" in text
def test_failure_renders_log_path_and_manual_command(self) -> None:
"""A failed install surfaces both the log path and the manual uv command."""
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_return=(False, "resolver: conflict"),
)
assert code == 1
text = self._printed_text(console_mock)
assert "Install failed" in text
assert "resolver: conflict" in text
assert "/tmp/deepagents-install.log" in text
assert "uv tool install -U 'deepagents-code[quickjs]'" in text
def test_keyboard_interrupt_exits_130(self) -> None:
"""Ctrl-C during install exits 130 with an Aborted message."""
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_side_effect=KeyboardInterrupt(),
)
assert code == 130
assert "Aborted" in self._printed_text(console_mock)
def test_unexpected_exception_includes_class_and_log(self) -> None:
"""Outer except prints the exception class, message, and log path."""
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_side_effect=RuntimeError("disk full"),
)
assert code == 1
text = self._printed_text(console_mock)
assert "RuntimeError" in text
assert "disk full" in text
assert "/tmp/deepagents-install.log" in text
assert "uv tool install -U 'deepagents-code[quickjs]'" in text
def test_interactive_decline_aborts(self) -> None:
"""Interactive TTY + reply 'n' to unknown extra aborts with exit 1."""
code, perform_mock, console_mock = self._run_install_capture(
"not-a-real-extra",
interactive=True,
input_reply="n",
)
assert code == 1
perform_mock.assert_not_awaited()
assert "Aborted" in self._printed_text(console_mock)
@@ -49,7 +49,9 @@ def _update_entry() -> PendingNotification:
NotificationAction(ActionId.SKIP_ONCE, "Remind me next launch"),
NotificationAction(ActionId.SKIP_VERSION, "Skip this version"),
),
payload=UpdateAvailablePayload(latest="2.0.0", upgrade_cmd="pip install"),
payload=UpdateAvailablePayload(
latest="2.0.0", upgrade_cmd="uv tool upgrade deepagents-code"
),
)
@@ -39,7 +39,9 @@ def _update_entry(
title=f"Update available: v{latest}",
body=f"v{latest} is available.",
actions=(NotificationAction(ActionId.INSTALL, "Install now", primary=True),),
payload=UpdateAvailablePayload(latest=latest, upgrade_cmd="pip install"),
payload=UpdateAvailablePayload(
latest=latest, upgrade_cmd="uv tool upgrade deepagents-code"
),
)
@@ -337,7 +337,7 @@ class TestVerifySandboxDeps:
pytest.raises(
ImportError,
match=rf"Missing dependencies for '{provider}' sandbox.*"
rf"pip install 'deepagents-code\[{provider}\]'",
rf"/install {provider}.*dcode --install {provider}",
),
):
verify_sandbox_deps(provider)
@@ -29,7 +29,9 @@ def _update_entry() -> PendingNotification:
NotificationAction(ActionId.SKIP_ONCE, "Remind me next launch"),
NotificationAction(ActionId.SKIP_VERSION, "Skip this version"),
),
payload=UpdateAvailablePayload(latest="2.0.0", upgrade_cmd="pip install"),
payload=UpdateAvailablePayload(
latest="2.0.0", upgrade_cmd="uv tool upgrade deepagents-code"
),
)
+235 -4
View File
@@ -20,6 +20,7 @@ from deepagents_code.update_check import (
cleanup_update_logs,
clear_update_notified,
create_update_log_path,
detect_install_method,
format_age_suffix,
format_installed_age_suffix,
format_release_age,
@@ -30,10 +31,14 @@ from deepagents_code.update_check import (
get_release_time,
get_sdk_release_time,
get_seen_version,
install_extra_command,
install_package_command,
is_auto_update_enabled,
is_update_available,
is_valid_extra_name,
mark_update_notified,
mark_version_seen,
perform_install_extra,
perform_upgrade,
set_auto_update,
should_notify_update,
@@ -788,6 +793,16 @@ class TestFormatInstalledAgeSuffix:
assert format_installed_age_suffix("1.0.0") == ""
class TestDetectInstallMethod:
def test_non_editable_non_uv_non_brew_returns_other(self) -> None:
"""The fallback bucket is not a positive pip detection."""
with (
patch("deepagents_code.update_check.sys.prefix", "/tmp/dcode-venv"),
patch("deepagents_code.config._is_editable_install", return_value=False),
):
assert detect_install_method() == "other"
class TestUpdateLogs:
def test_create_update_log_path_uses_log_dir(self, update_log_dir) -> None:
path = create_update_log_path()
@@ -823,11 +838,11 @@ class TestUpdateLogs:
with (
patch(
"deepagents_code.update_check.detect_install_method",
return_value="pip",
return_value="uv",
),
patch.dict(
"deepagents_code.update_check._UPGRADE_COMMANDS",
{"pip": "printf 'ok\\n'"},
{"uv": "printf 'ok\\n'"},
),
):
success, output = await perform_upgrade(log_path=log_path)
@@ -844,11 +859,11 @@ class TestUpdateLogs:
with (
patch(
"deepagents_code.update_check.detect_install_method",
return_value="pip",
return_value="uv",
),
patch.dict(
"deepagents_code.update_check._UPGRADE_COMMANDS",
{"pip": "printf 'ok\\n'"},
{"uv": "printf 'ok\\n'"},
),
patch("pathlib.Path.open", opener),
):
@@ -857,6 +872,222 @@ class TestUpdateLogs:
assert success is True
assert output == "ok"
async def test_perform_upgrade_refuses_other_install(self) -> None:
"""Unknown non-editable installs must not upgrade a separate uv tool env."""
with patch(
"deepagents_code.update_check.detect_install_method",
return_value="other",
):
success, output = await perform_upgrade()
assert success is False
assert "Unsupported install method" in output
class TestInstallExtraCommand:
"""`install_extra_command` builds the uv tool install string."""
def test_basic(self) -> None:
"""Single-quoted bracket form, with `-U` to reinstall."""
assert (
install_extra_command("quickjs")
== "uv tool install -U 'deepagents-code[quickjs]'"
)
def test_provider_extra(self) -> None:
assert (
install_extra_command("fireworks")
== "uv tool install -U 'deepagents-code[fireworks]'"
)
def test_rejects_shell_metacharacters(self) -> None:
assert not is_valid_extra_name("quickjs']; touch /tmp/pwned; '")
with pytest.raises(ValueError, match="Invalid extra name"):
install_extra_command("quickjs']; touch /tmp/pwned; '")
class TestInstallPackageCommand:
"""`install_package_command` builds a uv tool package install string."""
def test_basic(self) -> None:
assert (
install_package_command("langchain-custom")
== "uv tool install -U deepagents-code --with langchain-custom"
)
def test_allows_pep508_name_separators(self) -> None:
assert (
install_package_command("langchain.custom_provider")
== "uv tool install -U deepagents-code --with langchain.custom_provider"
)
def test_rejects_shell_metacharacters(self) -> None:
with pytest.raises(ValueError, match="Invalid package name"):
install_package_command("langchain-custom; touch /tmp/pwned")
class TestPerformInstallExtra:
"""`perform_install_extra` execution paths."""
async def test_editable_install_refuses(self) -> None:
"""Editable installs cannot accept extras via uv tool install."""
with patch(
"deepagents_code.update_check.detect_install_method",
return_value="unknown",
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert "Editable install" in output
assert "uv sync --extra quickjs" in output
async def test_brew_install_refuses(self) -> None:
"""Homebrew formula doesn't expose extras."""
with patch(
"deepagents_code.update_check.detect_install_method",
return_value="brew",
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert "Homebrew" in output
async def test_other_install_refuses(self) -> None:
"""Unknown non-editable installs cannot be updated through uv tool."""
with patch(
"deepagents_code.update_check.detect_install_method",
return_value="other",
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert "Unsupported install method" in output
async def test_invalid_extra_refuses_before_detecting_install(self) -> None:
"""Malformed forced extras must never reach command construction."""
with patch(
"deepagents_code.update_check.detect_install_method",
) as detect:
success, output = await perform_install_extra("quickjs']; echo nope; '")
assert success is False
assert "Invalid extra name" in output
detect.assert_not_called()
async def test_uv_install_runs(self, tmp_path) -> None:
"""`uv` method runs the subprocess and returns success."""
log_path = tmp_path / "install.log"
# Inject a no-op command in place of the real uv tool install so the
# subprocess actually exits 0 without touching the environment.
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.install_extra_command",
return_value="printf 'ok\\n'",
),
):
success, output = await perform_install_extra("quickjs", log_path=log_path)
assert success is True
assert output == "ok"
async def test_uv_missing_returns_actionable_error(self) -> None:
"""When `uv` is not on PATH, surface a clear error before exec."""
with (
patch(
"deepagents_code.update_check.detect_install_method",
return_value="uv",
),
patch(
"deepagents_code.update_check.shutil.which",
return_value=None,
),
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert "uv" in output
assert "not found" in output
class TestRunInstallSubprocessFailureModes:
"""Failure-mode coverage routed through `perform_install_extra`.
Exercises the shared `_run_install_subprocess` helper since it has no
public entry point of its own.
"""
async def test_timeout_kills_process(self, tmp_path) -> None:
"""A subprocess that exceeds `_UPGRADE_TIMEOUT` is killed and reported."""
log_path = tmp_path / "install.log"
with (
patch("deepagents_code.update_check._UPGRADE_TIMEOUT", 0.05),
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.install_extra_command",
return_value="sleep 5",
),
):
success, output = await perform_install_extra("quickjs", log_path=log_path)
assert success is False
assert "timed out" in output
async def test_oserror_includes_exception_detail(self, tmp_path) -> None:
"""An OSError during exec must surface the exception class + message."""
log_path = tmp_path / "install.log"
def _raise(*_args: object, **_kwargs: object) -> None:
raise FileNotFoundError(2, "No such file or directory", "uv")
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.install_extra_command",
return_value="uv tool install -U 'deepagents-code[quickjs]'",
),
patch("asyncio.create_subprocess_shell", side_effect=_raise),
):
success, output = await perform_install_extra("quickjs", log_path=log_path)
assert success is False
assert "FileNotFoundError" in output
assert "No such file" in output
async def test_nonzero_exit_returns_combined_output(self, tmp_path) -> None:
"""A failing subprocess returns False with stderr in the output."""
log_path = tmp_path / "install.log"
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.install_extra_command",
return_value="sh -c 'printf boom 1>&2; exit 1'",
),
):
success, output = await perform_install_extra("quickjs", log_path=log_path)
assert success is False
assert "boom" in output
def _mock_sdk_pypi_response(
releases: dict[str, list[dict[str, object]]] | None = None,
@@ -13,7 +13,7 @@ async def test_update_progress_screen_shows_tail_when_details_toggle(tmp_path) -
"""The progress modal keeps a bounded tail hidden until details are toggled."""
screen = UpdateProgressScreen(
latest="2.0.0",
command="pip install --upgrade deepagents-code",
command="uv tool upgrade deepagents-code",
log_path=tmp_path / "update.log",
tail_limit=2,
)
@@ -26,7 +26,7 @@ async def test_update_progress_screen_shows_tail_when_details_toggle(tmp_path) -
log_path = screen.query(Static).filter(".up-log").first()
assert details.display is False
assert log_path.display is False
assert "Running command: pip install --upgrade deepagents-code" in str(
assert "Running command: uv tool upgrade deepagents-code" in str(
details.render()
)
assert "tail -f" not in str(details.render())
@@ -50,7 +50,7 @@ async def test_update_progress_screen_copies_log_path_only_in_details(tmp_path)
log_path = tmp_path / "update.log"
screen = UpdateProgressScreen(
latest="2.0.0",
command="pip install --upgrade deepagents-code",
command="uv tool upgrade deepagents-code",
log_path=log_path,
)
@@ -96,7 +96,7 @@ async def test_update_progress_screen_close_waits_until_done(tmp_path) -> None:
"""Esc is ignored while the update is running and closes after completion."""
screen = UpdateProgressScreen(
latest="2.0.0",
command="pip install --upgrade deepagents-code",
command="uv tool upgrade deepagents-code",
log_path=tmp_path / "update.log",
)
+2 -2
View File
@@ -267,8 +267,8 @@ async def test_version_slash_command_omits_update_hint_when_up_to_date() -> None
async def test_update_slash_command_editable_install_short_circuits() -> None:
"""Editable install must not invoke `perform_upgrade` from the TUI.
A regression here would run `pip install --upgrade deepagents-code` on
an editable dev checkout and overwrite the local install.
A regression here would run `uv tool upgrade deepagents-code` on an
editable dev checkout and clobber the local install with a PyPI copy.
"""
from unittest.mock import AsyncMock
+4 -4
View File
@@ -1157,7 +1157,7 @@ runloop = [
together = [
{ name = "langchain-together" },
]
vertexai = [
vertex = [
{ name = "langchain-google-vertexai" },
]
xai = [
@@ -1191,7 +1191,7 @@ requires-dist = [
{ name = "deepagents", editable = "../deepagents" },
{ name = "deepagents-acp", specifier = ">=0.0.4" },
{ name = "deepagents-code", extras = ["agentcore", "daytona", "modal", "runloop"], marker = "extra == 'all-sandboxes'" },
{ name = "deepagents-code", extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertexai", "xai"], marker = "extra == 'all-providers'" },
{ name = "deepagents-code", extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertex", "xai"], marker = "extra == 'all-providers'" },
{ name = "httpx", specifier = ">=0.28.1,<1.0.0" },
{ name = "langchain", specifier = ">=1.3.2,<2.0.0" },
{ name = "langchain-agentcore-codeinterpreter", marker = "extra == 'agentcore'", specifier = ">=0.0.2" },
@@ -1205,7 +1205,7 @@ requires-dist = [
{ name = "langchain-fireworks", marker = "extra == 'fireworks'", specifier = ">=1.3.1,<2.0.0" },
{ name = "langchain-google-genai", specifier = ">=4.2.2,<5.0.0" },
{ name = "langchain-google-genai", marker = "extra == 'google-genai'", specifier = ">=4.2.2,<5.0.0" },
{ name = "langchain-google-vertexai", marker = "extra == 'vertexai'", specifier = ">=3.2.3,<4.0.0" },
{ name = "langchain-google-vertexai", marker = "extra == 'vertex'", specifier = ">=3.2.3,<4.0.0" },
{ name = "langchain-groq", marker = "extra == 'groq'", specifier = ">=1.1.2,<2.0.0" },
{ name = "langchain-huggingface", marker = "extra == 'huggingface'", specifier = ">=1.2.2,<2.0.0" },
{ name = "langchain-ibm", marker = "extra == 'ibm'", specifier = ">=1.0.7,<2.0.0" },
@@ -1244,7 +1244,7 @@ requires-dist = [
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
{ name = "uuid-utils", specifier = ">=0.10.0,<1.0.0" },
]
provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertexai", "xai", "all-providers", "agentcore", "daytona", "modal", "runloop", "all-sandboxes", "quickjs"]
provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertex", "xai", "all-providers", "agentcore", "daytona", "modal", "runloop", "all-sandboxes", "quickjs"]
[package.metadata.requires-dev]
test = [
+3 -3
View File
@@ -571,7 +571,7 @@ requires-dist = [
{ name = "deepagents", editable = "../deepagents" },
{ name = "deepagents-acp", specifier = ">=0.0.4" },
{ name = "deepagents-code", extras = ["agentcore", "daytona", "modal", "runloop"], marker = "extra == 'all-sandboxes'" },
{ name = "deepagents-code", extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertexai", "xai"], marker = "extra == 'all-providers'" },
{ name = "deepagents-code", extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertex", "xai"], marker = "extra == 'all-providers'" },
{ name = "httpx", specifier = ">=0.28.1,<1.0.0" },
{ name = "langchain", specifier = ">=1.3.2,<2.0.0" },
{ name = "langchain-agentcore-codeinterpreter", marker = "extra == 'agentcore'", specifier = ">=0.0.2" },
@@ -585,7 +585,7 @@ requires-dist = [
{ name = "langchain-fireworks", marker = "extra == 'fireworks'", specifier = ">=1.3.1,<2.0.0" },
{ name = "langchain-google-genai", specifier = ">=4.2.2,<5.0.0" },
{ name = "langchain-google-genai", marker = "extra == 'google-genai'", specifier = ">=4.2.2,<5.0.0" },
{ name = "langchain-google-vertexai", marker = "extra == 'vertexai'", specifier = ">=3.2.3,<4.0.0" },
{ name = "langchain-google-vertexai", marker = "extra == 'vertex'", specifier = ">=3.2.3,<4.0.0" },
{ name = "langchain-groq", marker = "extra == 'groq'", specifier = ">=1.1.2,<2.0.0" },
{ name = "langchain-huggingface", marker = "extra == 'huggingface'", specifier = ">=1.2.2,<2.0.0" },
{ name = "langchain-ibm", marker = "extra == 'ibm'", specifier = ">=1.0.7,<2.0.0" },
@@ -624,7 +624,7 @@ requires-dist = [
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
{ name = "uuid-utils", specifier = ">=0.10.0,<1.0.0" },
]
provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertexai", "xai", "all-providers", "agentcore", "daytona", "modal", "runloop", "all-sandboxes", "quickjs"]
provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fireworks", "google-genai", "groq", "huggingface", "ibm", "litellm", "mistralai", "nvidia", "ollama", "openai", "openrouter", "perplexity", "together", "vertex", "xai", "all-providers", "agentcore", "daytona", "modal", "runloop", "all-sandboxes", "quickjs"]
[package.metadata.requires-dev]
test = [