mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-08-27 20:50:04 -04:00
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:
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -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 = [
|
||||
|
||||
Generated
+3
-3
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user