mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
72aef52542
## ⚠ Breaking changes
`deepagents-cli` `0.1.x` → `0.2.0`. The deploy CLI is a private-preview
beta, but
this rewrites the command surface, config format, and deploy backend, so
existing
projects and scripts will not work unchanged.
| What broke | Before (`0.1.x`) | After (`0.2.0`) |
| --- | --- | --- |
| **`deepagents dev` removed** | `deepagents dev --port 2024` ran a
local `langgraph dev` server off the bundle | No replacement. Local
bundling/serving is gone; the project is now a managed agent. |
| **Deploy backend** | `deepagents deploy` bundled the project and
shipped via `langgraph deploy` to LangSmith Deployment | `deepagents
deploy` patches agent metadata + syncs source files through the Managed
Deep Agents API (`/v1/deepagents/*`) |
| **Config file** | `deepagents.toml` (auto-discovered or `--config`) |
`agent.json` (metadata, top-level `backend`, `runtime.model`,
permissions) + `AGENTS.md`, `tools.json`, `skills/`, `subagents/` |
| **`--config` / `--dry-run` flags** | accepted on `deploy`/`dev` |
removed |
| **MCP tool wiring** | prior tool-config shape (deployed an agent with
**no tools**) | `tools.json` with `tools[].mcp_server_url`; servers must
be registered and `can_invoke == true` or deploy fails fast |
| **Deploy state** | derived from the bundle | tracked user-local in
`~/.deepagents/deployments/`, keyed by project root + endpoint (not
committed) |
### Migration
1. **Re-scaffold or convert the project.** Run `deepagents init` to
generate the
canonical layout (`agent.json`, `AGENTS.md`, `tools.json`, `skills/`,
`subagents/`), then port settings out of your old `deepagents.toml`:
- system prompt → `AGENTS.md`
- model / permissions → `agent.json` (`runtime.model`, `permissions`)
- runtime/sandbox config → top-level `backend` block (`init` scaffolds
`thread_scoped_sandbox`; the legacy value `"sandbox"` is still accepted
as a
local alias)
2. **Register MCP servers and attach tools.** Use `deepagents
mcp-servers add`
(then `connect` for OAuth servers), and reference them from `tools.json`
via
`tools[].mcp_server_url`. The old tool-config shape silently deployed an
agent
with no tools.
3. **Replace `deepagents dev`.** There is no local dev-server equivalent
in this
release; iterate via `deepagents deploy` against your workspace.
4. **Drop removed flags.** Remove `--config` and `--dry-run` from any
deploy
scripts.
New commands: `deepagents agents` (`get`, `--include-files`, …) and
`deepagents mcp-servers` (`create`/`list`/`get`/`update`/`delete` +
OAuth
`connect`). `get` redacts stored header values.
## Overview
Migrates `deepagents deploy` from the legacy `langgraph deploy` path to
the new
Managed Deep Agents API (`/v1/deepagents/*`). Instead of bundling and
shipping a
deployment, the CLI now treats a project as a managed agent: agent
**metadata**
lives on the agent record and **source files** live in Hub-backed
directory
state. On redeploy, the CLI patches only metadata and syncs the managed
file tree
via Hub directory commits, rather than re-uploading everything.
The CLI surface is now `init` / `deploy` / `agents` / `mcp-servers`,
operating on
the canonical project layout:
```
agent.json # name, description, top-level backend, runtime.model, permissions
AGENTS.md # system prompt
tools.json # MCP tool references (tools[].mcp_server_url) + interrupt_config
skills/<name>/SKILL.md # frontmatter-tagged skills
subagents/<name>/ # delegated subagent definitions
```
Bumps `deepagents-cli` to `0.2.0`. The deploy flow is gated behind a
beta warning
while Managed Deep Agents is in private preview.
## What changed
**Deploy model**
- Source files are kept in Hub-backed directory state; redeploy patches
agent
metadata only and reconciles files through directory commits (create →
first
deploy, patch + directory delta → subsequent deploys).
- Deploy state is tracked **user-local** (`~/.deepagents/deployments/`),
keyed by
project root + endpoint — not committed to the repo.
**Runtime / backend config**
- Managed runtime config moved to a **top-level `backend`** block;
`init` scaffolds
`thread_scoped_sandbox` by default.
- Backend/sandbox config is validated at load time; legacy `"sandbox"`
is accepted
as a local alias for `thread_scoped_sandbox`.
**MCP server flows**
- Consistent dotenv loading across all commands (project `.env`
discovered upward,
then `~/.deepagents/.env`).
- `mcp-servers` supports create/list/get/update/delete plus OAuth
connect; `get`
redacts stored header values.
- Deploy fails fast when a referenced server is unregistered or the
caller
`can_invoke == false`, with actionable hints (including OAuth
`connect`).
**Tooling docs/examples** (also in this branch)
- `tools.json` (`tools[].mcp_server_url`) is the supported way to attach
MCP tools;
fixed the `deploy-content-writer` example and README quickstart, which
previously
implied a tool config shape that deploys an agent with no tools.
## Checked with
```bash
uv run --group test python -m pytest tests/unit_tests/deploy -v
uv run --group test ruff check .
uv run --group test ty check
```
Also ran the flow end to end against a live workspace: `init` →
`mcp-servers add`
→ `tools.json` → `deploy` (create + redeploy) → `agents get
--include-files`,
confirming metadata patch, directory sync, and MCP health check.
## Related
- Linear:
https://linear.app/langchain/issue/AB-2470/mda-wrap-apis-in-deepagents-deploy
- Docs PR: `langchain-ai/docs` `vic/mda-cli-update` (reconciles the
Managed Deep
Agents guide with this behavior)
BREAKING CHANGE: `deepagents deploy` now targets the Managed Deep Agents
API; `deepagents dev`, `deepagents.toml`, and the `--config`/`--dry-run`
flags are removed. Run `deepagents init` to re-scaffold (`agent.json` +
`tools.json`). See
[PR](https://github.com/langchain-ai/deepagents/pull/3609) for the full
migration guide.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
172 lines
5.4 KiB
Python
172 lines
5.4 KiB
Python
"""Project and global `.env` loading for the deploy CLI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
_PROJECT_DOTENV_BLOCKED_ENV_KEYS = (
|
|
"LANGSMITH_ENDPOINT",
|
|
"LANGCHAIN_ENDPOINT",
|
|
"HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"ALL_PROXY",
|
|
"NO_PROXY",
|
|
"http_proxy",
|
|
"https_proxy",
|
|
"all_proxy",
|
|
"no_proxy",
|
|
"SSL_CERT_FILE",
|
|
"SSL_CERT_DIR",
|
|
)
|
|
|
|
|
|
def _stderr(msg: str) -> None:
|
|
"""Print a diagnostic to stderr without a trailing newline corruption.
|
|
|
|
The CLI does not configure logging handlers by default, so `logger.warning`
|
|
output is invisible to end users. Diagnostics that the user authored — like
|
|
a malformed `.env` they meant to load — should also surface on stderr.
|
|
"""
|
|
sys.stderr.write(f"warning: {msg}\n")
|
|
|
|
|
|
def _find_dotenv_from_start_path(start_path: Path) -> Path | None:
|
|
"""Find the nearest `.env` file from an explicit start path upward.
|
|
|
|
Args:
|
|
start_path: Directory to start searching from.
|
|
|
|
Returns:
|
|
Path to the nearest `.env` file, or `None` if not found.
|
|
"""
|
|
current = start_path.expanduser().resolve()
|
|
for parent in [current, *list(current.parents)]:
|
|
candidate = parent / ".env"
|
|
try:
|
|
if candidate.is_file():
|
|
return candidate
|
|
except OSError:
|
|
logger.warning("Could not inspect .env candidate %s", candidate)
|
|
continue
|
|
return None
|
|
|
|
|
|
def _snapshot_blocked_project_env() -> dict[str, str | None]:
|
|
return {key: os.environ.get(key) for key in _PROJECT_DOTENV_BLOCKED_ENV_KEYS}
|
|
|
|
|
|
def _restore_env(snapshot: dict[str, str | None]) -> None:
|
|
for key, value in snapshot.items():
|
|
if value is None:
|
|
os.environ.pop(key, None)
|
|
else:
|
|
os.environ[key] = value
|
|
|
|
|
|
def _warn_if_project_blocked_env_changed(snapshot: dict[str, str | None]) -> None:
|
|
changed = [key for key, value in snapshot.items() if os.environ.get(key) != value]
|
|
if not changed:
|
|
return
|
|
keys = ", ".join(sorted(changed))
|
|
_stderr(
|
|
f"ignoring {keys} from project .env. Set endpoint overrides in your "
|
|
"shell environment or ~/.deepagents/.env; proxy/TLS settings are "
|
|
"ignored for managed API requests.",
|
|
)
|
|
|
|
|
|
try:
|
|
_GLOBAL_DOTENV_PATH = Path.home() / ".deepagents" / ".env"
|
|
except RuntimeError:
|
|
# `Path.home()` raises when `HOME` is unset/misconfigured (e.g. on some
|
|
# CI runners). Fall back to a path that won't exist so `is_file()`
|
|
# short-circuits cleanly, and surface the suppression on stderr so the
|
|
# user knows global defaults will not apply.
|
|
_GLOBAL_DOTENV_PATH = Path("/nonexistent/.deepagents/.env")
|
|
_stderr(
|
|
"could not resolve home directory; "
|
|
"global `~/.deepagents/.env` will not be loaded",
|
|
)
|
|
logger.warning(
|
|
"Could not resolve home directory; global .env will not be loaded",
|
|
exc_info=True,
|
|
)
|
|
|
|
|
|
def _load_dotenv(*, start_path: Path) -> bool:
|
|
"""Load environment variables from project and global `.env` files.
|
|
|
|
Loads in order (first write wins, `override=False`):
|
|
|
|
1. Project `.env` — discovered upward from `start_path`.
|
|
2. `~/.deepagents/.env` — global user defaults.
|
|
|
|
Both layers use `override=False` so that shell-exported variables always
|
|
take precedence over dotenv files. Because the project file loads first,
|
|
the effective precedence is:
|
|
|
|
```text
|
|
shell env > project `.env` > global `.env`
|
|
```
|
|
|
|
When a *resolved* dotenv path fails to parse the user is notified on
|
|
stderr in addition to the structured `logger.warning` (the CLI does not
|
|
install logging handlers by default).
|
|
|
|
Args:
|
|
start_path: Directory to use for project `.env` discovery. Searched
|
|
upward until either a `.env` is found or the filesystem root is
|
|
reached.
|
|
|
|
Returns:
|
|
`True` when at least one dotenv file was loaded, `False` otherwise.
|
|
"""
|
|
import dotenv
|
|
|
|
loaded = False
|
|
|
|
dotenv_path = _find_dotenv_from_start_path(start_path)
|
|
if dotenv_path is not None:
|
|
blocked_env = _snapshot_blocked_project_env()
|
|
try:
|
|
loaded = (
|
|
dotenv.load_dotenv(dotenv_path=dotenv_path, override=False) or loaded
|
|
)
|
|
_warn_if_project_blocked_env_changed(blocked_env)
|
|
except (OSError, ValueError) as exc:
|
|
_stderr(
|
|
f"could not read project .env at {dotenv_path}: {exc}. "
|
|
"Project env vars will not be loaded.",
|
|
)
|
|
logger.warning(
|
|
"Could not read project dotenv at %s; "
|
|
"project env vars will not be loaded",
|
|
dotenv_path,
|
|
exc_info=True,
|
|
)
|
|
finally:
|
|
_restore_env(blocked_env)
|
|
|
|
try:
|
|
if _GLOBAL_DOTENV_PATH.is_file() and dotenv.load_dotenv(
|
|
dotenv_path=_GLOBAL_DOTENV_PATH, override=False
|
|
):
|
|
loaded = True
|
|
logger.debug("Loaded global dotenv: %s", _GLOBAL_DOTENV_PATH)
|
|
except (OSError, ValueError) as exc:
|
|
_stderr(
|
|
f"could not read global .env at {_GLOBAL_DOTENV_PATH}: {exc}. "
|
|
"Global defaults will not be applied.",
|
|
)
|
|
logger.warning(
|
|
"Could not read global dotenv at %s; global defaults will not be applied",
|
|
_GLOBAL_DOTENV_PATH,
|
|
exc_info=True,
|
|
)
|
|
|
|
return loaded
|