zjcode 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
"""Sandbox lifecycle management with provider abstraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import shlex
|
|
11
|
+
import string
|
|
12
|
+
import time
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from datetime import timedelta
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
17
|
+
|
|
18
|
+
from rich.markup import escape as escape_markup
|
|
19
|
+
|
|
20
|
+
from deepagents_code.config import console, get_glyphs
|
|
21
|
+
from deepagents_code.integrations.sandbox_provider import (
|
|
22
|
+
SandboxNotFoundError,
|
|
23
|
+
SandboxProvider,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Generator
|
|
30
|
+
from types import ModuleType
|
|
31
|
+
|
|
32
|
+
from deepagents.backends.protocol import SandboxBackendProtocol
|
|
33
|
+
|
|
34
|
+
from deepagents_code.integrations.sandbox_registry import SandboxRegistry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run_sandbox_setup(backend: SandboxBackendProtocol, setup_script_path: str) -> None:
|
|
38
|
+
"""Run users setup script in sandbox with env var expansion.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
backend: Sandbox backend instance
|
|
42
|
+
setup_script_path: Path to setup script file
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
FileNotFoundError: If the setup script does not exist.
|
|
46
|
+
RuntimeError: If the setup script fails to execute.
|
|
47
|
+
"""
|
|
48
|
+
script_path = Path(setup_script_path)
|
|
49
|
+
if not script_path.exists():
|
|
50
|
+
msg = f"Setup script not found: {setup_script_path}"
|
|
51
|
+
raise FileNotFoundError(msg)
|
|
52
|
+
|
|
53
|
+
console.print(
|
|
54
|
+
f"[dim]Running setup script: {escape_markup(setup_script_path)}...[/dim]"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Read script content
|
|
58
|
+
script_content = script_path.read_text(encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
# Expand ${VAR} syntax using local environment
|
|
61
|
+
template = string.Template(script_content)
|
|
62
|
+
expanded_script = template.safe_substitute(os.environ)
|
|
63
|
+
|
|
64
|
+
# Execute expanded script in sandbox
|
|
65
|
+
result = backend.execute(f"bash -c {shlex.quote(expanded_script)}")
|
|
66
|
+
|
|
67
|
+
if result.exit_code != 0:
|
|
68
|
+
console.print(f"[red]Setup script failed (exit {result.exit_code}):[/red]")
|
|
69
|
+
console.print(f"[dim]{escape_markup(result.output)}[/dim]")
|
|
70
|
+
msg = "Setup failed - aborting"
|
|
71
|
+
raise RuntimeError(msg)
|
|
72
|
+
|
|
73
|
+
console.print(f"[green]{get_glyphs().checkmark} Setup complete[/green]")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@contextmanager
|
|
77
|
+
def create_sandbox(
|
|
78
|
+
provider: str,
|
|
79
|
+
*,
|
|
80
|
+
sandbox_id: str | None = None,
|
|
81
|
+
snapshot_name: str | None = None,
|
|
82
|
+
setup_script_path: str | None = None,
|
|
83
|
+
params: dict[str, Any] | None = None,
|
|
84
|
+
) -> Generator[SandboxBackendProtocol, None, None]:
|
|
85
|
+
"""Create or connect to a sandbox of the specified provider.
|
|
86
|
+
|
|
87
|
+
This is the unified interface for sandbox creation using the
|
|
88
|
+
provider abstraction.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
provider: Sandbox provider name. Built-ins (`'agentcore'`, `'daytona'`,
|
|
92
|
+
`'langsmith'`, `'modal'`, `'runloop'`, `'vercel'`), entry-point
|
|
93
|
+
providers, and config-declared providers are all resolved through
|
|
94
|
+
the registry.
|
|
95
|
+
sandbox_id: Optional existing sandbox ID to reuse
|
|
96
|
+
snapshot_name: Optional sandbox snapshot name to use or create.
|
|
97
|
+
Honored by providers whose metadata sets `supports_snapshot_name`
|
|
98
|
+
(built-ins: `'langsmith'` snapshot, `'runloop'` blueprint); must be
|
|
99
|
+
`None` for other providers.
|
|
100
|
+
setup_script_path: Optional path to setup script to run after sandbox starts
|
|
101
|
+
params: Extra keyword arguments forwarded to `provider.get_or_create()`
|
|
102
|
+
(e.g. config-declared `[sandboxes.providers.<name>.params]`).
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
`SandboxBackendProtocol` instance
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If `snapshot_name` is provided for an unsupported provider,
|
|
109
|
+
or combined with `sandbox_id` (snapshots only apply to fresh sandboxes).
|
|
110
|
+
"""
|
|
111
|
+
registry = _get_registry()
|
|
112
|
+
metadata = registry.get_metadata(provider)
|
|
113
|
+
if snapshot_name is not None and (
|
|
114
|
+
metadata is None or not metadata.supports_snapshot_name
|
|
115
|
+
):
|
|
116
|
+
msg = (
|
|
117
|
+
f"snapshot_name is not supported by provider {provider!r} "
|
|
118
|
+
f"(got snapshot_name={snapshot_name!r})"
|
|
119
|
+
)
|
|
120
|
+
raise ValueError(msg)
|
|
121
|
+
if snapshot_name is not None and sandbox_id is not None:
|
|
122
|
+
msg = (
|
|
123
|
+
"snapshot_name cannot be combined with sandbox_id; "
|
|
124
|
+
"snapshots only apply when creating a fresh sandbox"
|
|
125
|
+
)
|
|
126
|
+
raise ValueError(msg)
|
|
127
|
+
|
|
128
|
+
# Get provider instance (reuse the registry already built above so we
|
|
129
|
+
# don't re-read the config file and re-scan entry points).
|
|
130
|
+
provider_obj = _get_provider(provider, registry=registry)
|
|
131
|
+
|
|
132
|
+
# Determine if we should cleanup (only cleanup if we created it)
|
|
133
|
+
should_cleanup = sandbox_id is None
|
|
134
|
+
provider_kwargs: dict[str, Any] = dict(registry.get_params(provider))
|
|
135
|
+
if params:
|
|
136
|
+
provider_kwargs.update(params)
|
|
137
|
+
if snapshot_name is not None:
|
|
138
|
+
provider_kwargs["snapshot"] = snapshot_name
|
|
139
|
+
|
|
140
|
+
# Create or connect to sandbox
|
|
141
|
+
console.print(f"[yellow]Starting {provider} sandbox...[/yellow]")
|
|
142
|
+
backend = provider_obj.get_or_create(sandbox_id=sandbox_id, **provider_kwargs)
|
|
143
|
+
glyphs = get_glyphs()
|
|
144
|
+
console.print(
|
|
145
|
+
f"[green]{glyphs.checkmark} {provider.capitalize()} sandbox ready: "
|
|
146
|
+
f"{backend.id}[/green]"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Run setup script if provided
|
|
150
|
+
if setup_script_path:
|
|
151
|
+
_run_sandbox_setup(backend, setup_script_path)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
yield backend
|
|
155
|
+
finally:
|
|
156
|
+
if should_cleanup:
|
|
157
|
+
try:
|
|
158
|
+
console.print(
|
|
159
|
+
f"[dim]Terminating {provider} sandbox {backend.id}...[/dim]"
|
|
160
|
+
)
|
|
161
|
+
provider_obj.delete(sandbox_id=backend.id)
|
|
162
|
+
glyphs = get_glyphs()
|
|
163
|
+
console.print(
|
|
164
|
+
f"[dim]{glyphs.checkmark} {provider.capitalize()} sandbox "
|
|
165
|
+
f"{backend.id} terminated[/dim]"
|
|
166
|
+
)
|
|
167
|
+
except Exception as e: # noqa: BLE001 # Cleanup errors should not mask the original sandbox failure
|
|
168
|
+
warning = get_glyphs().warning
|
|
169
|
+
console.print(
|
|
170
|
+
f"[yellow]{warning} Cleanup failed for {provider} sandbox "
|
|
171
|
+
f"{backend.id}: {e}[/yellow]"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _get_registry() -> SandboxRegistry:
|
|
176
|
+
"""Build a `SandboxRegistry` from the current user config.
|
|
177
|
+
|
|
178
|
+
Not cached: each call re-reads the config file and re-scans entry points so
|
|
179
|
+
the registry reflects the latest state. Reuse a single instance within one
|
|
180
|
+
operation (see `create_sandbox`) rather than calling this repeatedly.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A fresh `SandboxRegistry`.
|
|
184
|
+
"""
|
|
185
|
+
from deepagents_code.integrations.sandbox_registry import SandboxRegistry
|
|
186
|
+
|
|
187
|
+
return SandboxRegistry.load()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_default_working_dir(provider: str) -> str:
|
|
191
|
+
"""Get the default working directory for a given sandbox provider.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
provider: Sandbox provider name. Resolved through the registry so
|
|
195
|
+
built-in, entry-point, and config providers are all supported.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Default working directory path as string
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If provider is unknown
|
|
202
|
+
"""
|
|
203
|
+
metadata = _get_registry().get_metadata(provider)
|
|
204
|
+
if metadata is None:
|
|
205
|
+
msg = f"Unknown sandbox provider: {provider}"
|
|
206
|
+
raise ValueError(msg)
|
|
207
|
+
return metadata.working_dir
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Provider implementations
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _import_provider_module(
|
|
216
|
+
module_name: str,
|
|
217
|
+
*,
|
|
218
|
+
provider: str,
|
|
219
|
+
package: str,
|
|
220
|
+
) -> ModuleType:
|
|
221
|
+
"""Import an optional provider module with a provider-specific error message.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
module_name: Python module name to import.
|
|
225
|
+
provider: Sandbox provider name (e.g. `'daytona'`).
|
|
226
|
+
package: PyPI package name exposed by the package extra.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The imported module object.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ImportError: If the optional dependency is not installed.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
return importlib.import_module(module_name)
|
|
236
|
+
except ImportError as exc:
|
|
237
|
+
msg = (
|
|
238
|
+
f"The '{provider}' sandbox provider requires the '{package}' package. "
|
|
239
|
+
f"Install it with: /install {provider} (in-app) or "
|
|
240
|
+
f"dcode --install {provider} (CLI)"
|
|
241
|
+
)
|
|
242
|
+
raise ImportError(msg) from exc
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_LANGSMITH_DEFAULT_SNAPSHOT = "deepagents-code"
|
|
246
|
+
"""Default LangSmith sandbox snapshot name used when none is specified."""
|
|
247
|
+
|
|
248
|
+
_LANGSMITH_DEFAULT_IMAGE = "python:3"
|
|
249
|
+
"""Default Docker image for LangSmith sandbox snapshots when none is provided."""
|
|
250
|
+
|
|
251
|
+
_LANGSMITH_DEFAULT_FS_CAPACITY_BYTES = 16 * 1024**3
|
|
252
|
+
"""Default filesystem capacity (16 GiB) for LangSmith sandbox snapshots."""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class _LangSmithProvider(SandboxProvider):
|
|
256
|
+
"""LangSmith sandbox provider implementation.
|
|
257
|
+
|
|
258
|
+
Manages LangSmith sandbox lifecycle using the LangSmith SDK, booting
|
|
259
|
+
sandboxes from snapshots built from a Docker image.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
263
|
+
"""Initialize LangSmith provider.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
api_key: LangSmith API key (defaults to `LANGSMITH_SANDBOX_API_KEY`,
|
|
267
|
+
then `LANGSMITH_API_KEY` env var).
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ValueError: If no LangSmith API key is found.
|
|
271
|
+
"""
|
|
272
|
+
from langsmith.sandbox import SandboxClient
|
|
273
|
+
|
|
274
|
+
from deepagents_code.model_config import resolve_env_var
|
|
275
|
+
|
|
276
|
+
sandbox_key = resolve_env_var("LANGSMITH_SANDBOX_API_KEY")
|
|
277
|
+
if sandbox_key:
|
|
278
|
+
logger.debug("Using LangSmith API key from LANGSMITH_SANDBOX_API_KEY")
|
|
279
|
+
self._api_key: str | None = (
|
|
280
|
+
api_key
|
|
281
|
+
or sandbox_key
|
|
282
|
+
or resolve_env_var("LANGSMITH_API_KEY")
|
|
283
|
+
or resolve_env_var("LANGCHAIN_API_KEY")
|
|
284
|
+
)
|
|
285
|
+
if not self._api_key:
|
|
286
|
+
msg = (
|
|
287
|
+
"No LangSmith sandbox API key found. Set "
|
|
288
|
+
"LANGSMITH_API_KEY, LANGCHAIN_API_KEY, or LANGSMITH_SANDBOX_API_KEY "
|
|
289
|
+
"(or the DEEPAGENTS_CODE_-prefixed equivalents)."
|
|
290
|
+
)
|
|
291
|
+
raise ValueError(msg)
|
|
292
|
+
self._client: SandboxClient = SandboxClient(api_key=self._api_key)
|
|
293
|
+
|
|
294
|
+
def get_or_create(
|
|
295
|
+
self,
|
|
296
|
+
*,
|
|
297
|
+
sandbox_id: str | None = None,
|
|
298
|
+
timeout: int = 180,
|
|
299
|
+
snapshot: str | None = None,
|
|
300
|
+
snapshot_image: str | None = None,
|
|
301
|
+
fs_capacity_bytes: int | None = None,
|
|
302
|
+
**kwargs: Any,
|
|
303
|
+
) -> SandboxBackendProtocol:
|
|
304
|
+
"""Get existing or create new LangSmith sandbox.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
sandbox_id: Optional existing sandbox name to reuse.
|
|
308
|
+
timeout: Timeout in seconds for sandbox startup.
|
|
309
|
+
snapshot: Snapshot name to boot from.
|
|
310
|
+
|
|
311
|
+
Resolved to a snapshot ID, creating the snapshot from
|
|
312
|
+
`snapshot_image` if missing. Overrides
|
|
313
|
+
`LANGSMITH_SANDBOX_SNAPSHOT_NAME`; overridden by
|
|
314
|
+
`LANGSMITH_SANDBOX_SNAPSHOT_ID` (ID, wins over everything).
|
|
315
|
+
snapshot_image: Docker image used when building the snapshot.
|
|
316
|
+
fs_capacity_bytes: Filesystem capacity when building the snapshot.
|
|
317
|
+
**kwargs: Additional LangSmith-specific parameters.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
`LangSmithSandbox` instance.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
RuntimeError: If sandbox connection or startup fails.
|
|
324
|
+
TypeError: If unsupported keyword arguments are provided.
|
|
325
|
+
"""
|
|
326
|
+
from deepagents.backends.langsmith import LangSmithSandbox
|
|
327
|
+
|
|
328
|
+
from deepagents_code.model_config import resolve_env_var
|
|
329
|
+
|
|
330
|
+
if kwargs:
|
|
331
|
+
msg = f"Received unsupported arguments: {list(kwargs.keys())}"
|
|
332
|
+
raise TypeError(msg)
|
|
333
|
+
if sandbox_id:
|
|
334
|
+
# Connect to existing sandbox by name
|
|
335
|
+
try:
|
|
336
|
+
sandbox = self._client.get_sandbox(name=sandbox_id)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
msg = f"Failed to connect to existing sandbox '{sandbox_id}': {e}"
|
|
339
|
+
raise RuntimeError(msg) from e
|
|
340
|
+
return LangSmithSandbox(sandbox)
|
|
341
|
+
|
|
342
|
+
# Explicit snapshot ID wins — skip name lookup and auto-build.
|
|
343
|
+
env_snapshot_id = resolve_env_var("LANGSMITH_SANDBOX_SNAPSHOT_ID")
|
|
344
|
+
if env_snapshot_id:
|
|
345
|
+
snapshot_id = env_snapshot_id
|
|
346
|
+
snapshot_name = env_snapshot_id
|
|
347
|
+
else:
|
|
348
|
+
env_snapshot_name = resolve_env_var("LANGSMITH_SANDBOX_SNAPSHOT_NAME")
|
|
349
|
+
snapshot_name = snapshot or env_snapshot_name or _LANGSMITH_DEFAULT_SNAPSHOT
|
|
350
|
+
image = snapshot_image or _LANGSMITH_DEFAULT_IMAGE
|
|
351
|
+
capacity = fs_capacity_bytes or _LANGSMITH_DEFAULT_FS_CAPACITY_BYTES
|
|
352
|
+
snapshot_id = self._ensure_snapshot(snapshot_name, image, capacity)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
sandbox = self._client.create_sandbox(
|
|
356
|
+
snapshot_id=snapshot_id, timeout=timeout
|
|
357
|
+
)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
msg = f"Failed to create sandbox from snapshot '{snapshot_name}': {e}"
|
|
360
|
+
raise RuntimeError(msg) from e
|
|
361
|
+
|
|
362
|
+
# Verify sandbox is ready by polling
|
|
363
|
+
for _ in range(timeout // 2):
|
|
364
|
+
try:
|
|
365
|
+
result = sandbox.run("echo ready", timeout=5)
|
|
366
|
+
if result.exit_code == 0:
|
|
367
|
+
break
|
|
368
|
+
except Exception: # noqa: S110, BLE001 # Sandbox not ready yet, continue polling
|
|
369
|
+
pass
|
|
370
|
+
time.sleep(2)
|
|
371
|
+
else:
|
|
372
|
+
# Cleanup on failure
|
|
373
|
+
with contextlib.suppress(Exception):
|
|
374
|
+
self._client.delete_sandbox(sandbox.name)
|
|
375
|
+
msg = f"LangSmith sandbox failed to start within {timeout} seconds"
|
|
376
|
+
raise RuntimeError(msg)
|
|
377
|
+
|
|
378
|
+
return LangSmithSandbox(sandbox)
|
|
379
|
+
|
|
380
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # Required by SandboxFactory interface
|
|
381
|
+
"""Delete a LangSmith sandbox.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
sandbox_id: Sandbox name to delete
|
|
385
|
+
**kwargs: Additional parameters
|
|
386
|
+
"""
|
|
387
|
+
self._client.delete_sandbox(sandbox_id)
|
|
388
|
+
|
|
389
|
+
def _ensure_snapshot(
|
|
390
|
+
self,
|
|
391
|
+
snapshot_name: str,
|
|
392
|
+
image: str,
|
|
393
|
+
fs_capacity_bytes: int,
|
|
394
|
+
) -> str:
|
|
395
|
+
"""Resolve a snapshot by name, building it from `image` if missing.
|
|
396
|
+
|
|
397
|
+
The LangSmith API exposes snapshots by ID, so we list and filter by
|
|
398
|
+
name. Only snapshots with `status == "ready"` are returned; a
|
|
399
|
+
matching-name snapshot in a non-ready state (`"building"`,
|
|
400
|
+
`"failed"`, etc.) raises rather than triggering a duplicate build,
|
|
401
|
+
which would mask the in-flight/failed snapshot.
|
|
402
|
+
|
|
403
|
+
When no matching snapshot exists at all, we build one with
|
|
404
|
+
`create_snapshot`, which blocks until the snapshot is ready.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
The snapshot ID ready to be passed to `create_sandbox`.
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
RuntimeError: If listing or building the snapshot fails, or if
|
|
411
|
+
a matching-name snapshot exists but is not ready.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
snapshots = self._client.list_snapshots()
|
|
415
|
+
except Exception as e:
|
|
416
|
+
msg = f"Failed to list snapshots: {e}"
|
|
417
|
+
raise RuntimeError(msg) from e
|
|
418
|
+
|
|
419
|
+
non_ready_status: str | None = None
|
|
420
|
+
for snap in snapshots:
|
|
421
|
+
if snap.name != snapshot_name:
|
|
422
|
+
continue
|
|
423
|
+
if snap.status == "ready":
|
|
424
|
+
return snap.id
|
|
425
|
+
non_ready_status = snap.status
|
|
426
|
+
|
|
427
|
+
if non_ready_status is not None:
|
|
428
|
+
msg = (
|
|
429
|
+
f"Snapshot '{snapshot_name}' exists but is in state "
|
|
430
|
+
f"'{non_ready_status}'. Wait for it to finish building, or "
|
|
431
|
+
f"delete it to rebuild."
|
|
432
|
+
)
|
|
433
|
+
raise RuntimeError(msg)
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
snapshot = self._client.create_snapshot(
|
|
437
|
+
name=snapshot_name,
|
|
438
|
+
docker_image=image,
|
|
439
|
+
fs_capacity_bytes=fs_capacity_bytes,
|
|
440
|
+
)
|
|
441
|
+
except Exception as create_err:
|
|
442
|
+
msg = f"Failed to build snapshot '{snapshot_name}': {create_err}"
|
|
443
|
+
raise RuntimeError(msg) from create_err
|
|
444
|
+
return snapshot.id
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class _DaytonaProvider(SandboxProvider):
|
|
448
|
+
"""Daytona sandbox provider — lifecycle management for Daytona sandboxes."""
|
|
449
|
+
|
|
450
|
+
def __init__(self) -> None:
|
|
451
|
+
daytona_module = _import_provider_module(
|
|
452
|
+
"daytona",
|
|
453
|
+
provider="daytona",
|
|
454
|
+
package="langchain-daytona",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
from deepagents_code.model_config import resolve_env_var
|
|
458
|
+
|
|
459
|
+
api_key = resolve_env_var("DAYTONA_API_KEY")
|
|
460
|
+
if not api_key:
|
|
461
|
+
msg = (
|
|
462
|
+
"No Daytona API key found. Set DAYTONA_API_KEY "
|
|
463
|
+
"or DEEPAGENTS_CODE_DAYTONA_API_KEY."
|
|
464
|
+
)
|
|
465
|
+
raise ValueError(msg)
|
|
466
|
+
self._client = daytona_module.Daytona(
|
|
467
|
+
daytona_module.DaytonaConfig(
|
|
468
|
+
api_key=api_key,
|
|
469
|
+
api_url=resolve_env_var("DAYTONA_API_URL"),
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def get_or_create(
|
|
474
|
+
self,
|
|
475
|
+
*,
|
|
476
|
+
sandbox_id: str | None = None,
|
|
477
|
+
timeout: int = 180,
|
|
478
|
+
**kwargs: Any, # noqa: ARG002
|
|
479
|
+
) -> SandboxBackendProtocol:
|
|
480
|
+
"""Get or create a Daytona sandbox.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
sandbox_id: Not supported yet — must be None.
|
|
484
|
+
timeout: Seconds to wait for startup.
|
|
485
|
+
**kwargs: Unused.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
`DaytonaSandbox` instance.
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
NotImplementedError: If `sandbox_id` is provided.
|
|
492
|
+
RuntimeError: If the sandbox fails to start.
|
|
493
|
+
"""
|
|
494
|
+
daytona_backend = _import_provider_module(
|
|
495
|
+
"langchain_daytona",
|
|
496
|
+
provider="daytona",
|
|
497
|
+
package="langchain-daytona",
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if sandbox_id:
|
|
501
|
+
msg = (
|
|
502
|
+
"Connecting to existing Daytona sandbox by ID not yet supported. "
|
|
503
|
+
"Create a new sandbox by omitting sandbox_id parameter."
|
|
504
|
+
)
|
|
505
|
+
raise NotImplementedError(msg)
|
|
506
|
+
|
|
507
|
+
sandbox = self._client.create()
|
|
508
|
+
last_exc: Exception | None = None
|
|
509
|
+
for _ in range(timeout // 2):
|
|
510
|
+
try:
|
|
511
|
+
result = sandbox.process.exec("echo ready", timeout=5)
|
|
512
|
+
if result.exit_code == 0:
|
|
513
|
+
break
|
|
514
|
+
except Exception as exc: # noqa: BLE001 # Transient failures expected during readiness polling
|
|
515
|
+
last_exc = exc
|
|
516
|
+
time.sleep(2)
|
|
517
|
+
else:
|
|
518
|
+
with contextlib.suppress(Exception): # Best-effort cleanup
|
|
519
|
+
sandbox.delete()
|
|
520
|
+
detail = f" Last error: {last_exc}" if last_exc else ""
|
|
521
|
+
msg = f"Daytona sandbox failed to start within {timeout} seconds.{detail}"
|
|
522
|
+
raise RuntimeError(msg)
|
|
523
|
+
|
|
524
|
+
return daytona_backend.DaytonaSandbox(sandbox=sandbox)
|
|
525
|
+
|
|
526
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
|
|
527
|
+
"""Delete a Daytona sandbox by id."""
|
|
528
|
+
sandbox = self._client.get(sandbox_id)
|
|
529
|
+
self._client.delete(sandbox)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class _ModalProvider(SandboxProvider):
|
|
533
|
+
"""Modal sandbox provider — lifecycle management for Modal sandboxes."""
|
|
534
|
+
|
|
535
|
+
def __init__(self) -> None:
|
|
536
|
+
self._modal = _import_provider_module(
|
|
537
|
+
"modal",
|
|
538
|
+
provider="modal",
|
|
539
|
+
package="langchain-modal",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
from deepagents_code.model_config import resolve_env_var
|
|
543
|
+
|
|
544
|
+
token_id = resolve_env_var("MODAL_TOKEN_ID")
|
|
545
|
+
token_secret = resolve_env_var("MODAL_TOKEN_SECRET")
|
|
546
|
+
if token_id and token_secret:
|
|
547
|
+
try:
|
|
548
|
+
self._client = self._modal.Client.from_credentials(
|
|
549
|
+
token_id, token_secret
|
|
550
|
+
)
|
|
551
|
+
except Exception as exc:
|
|
552
|
+
msg = (
|
|
553
|
+
"Failed to authenticate with Modal using "
|
|
554
|
+
"MODAL_TOKEN_ID / MODAL_TOKEN_SECRET "
|
|
555
|
+
"(or the DEEPAGENTS_CODE_-prefixed equivalents). "
|
|
556
|
+
"Verify your credentials are valid."
|
|
557
|
+
)
|
|
558
|
+
raise ValueError(msg) from exc
|
|
559
|
+
elif token_id or token_secret:
|
|
560
|
+
logger.warning(
|
|
561
|
+
"Only one of MODAL_TOKEN_ID / MODAL_TOKEN_SECRET is set; "
|
|
562
|
+
"both are required for explicit credential auth. "
|
|
563
|
+
"Falling back to default Modal authentication.",
|
|
564
|
+
)
|
|
565
|
+
self._client = None
|
|
566
|
+
else:
|
|
567
|
+
self._client = None
|
|
568
|
+
|
|
569
|
+
lookup_kwargs: dict[str, Any] = {
|
|
570
|
+
"name": "deepagents-sandbox",
|
|
571
|
+
"create_if_missing": True,
|
|
572
|
+
}
|
|
573
|
+
if self._client is not None:
|
|
574
|
+
lookup_kwargs["client"] = self._client
|
|
575
|
+
self._app = self._modal.App.lookup(**lookup_kwargs)
|
|
576
|
+
|
|
577
|
+
def get_or_create(
|
|
578
|
+
self,
|
|
579
|
+
*,
|
|
580
|
+
sandbox_id: str | None = None,
|
|
581
|
+
timeout: int = 180,
|
|
582
|
+
**kwargs: Any, # noqa: ARG002
|
|
583
|
+
) -> SandboxBackendProtocol:
|
|
584
|
+
"""Get or create a Modal sandbox.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
sandbox_id: Existing sandbox ID, or None to create.
|
|
588
|
+
timeout: Seconds to wait for startup.
|
|
589
|
+
**kwargs: Unused.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
`ModalSandbox` instance.
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
RuntimeError: If the sandbox fails to start.
|
|
596
|
+
"""
|
|
597
|
+
modal_backend = _import_provider_module(
|
|
598
|
+
"langchain_modal",
|
|
599
|
+
provider="modal",
|
|
600
|
+
package="langchain-modal",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
client_kwargs: dict[str, Any] = {}
|
|
604
|
+
if self._client is not None:
|
|
605
|
+
client_kwargs["client"] = self._client
|
|
606
|
+
|
|
607
|
+
if sandbox_id:
|
|
608
|
+
sandbox = self._modal.Sandbox.from_id(
|
|
609
|
+
sandbox_id=sandbox_id,
|
|
610
|
+
app=self._app,
|
|
611
|
+
**client_kwargs,
|
|
612
|
+
)
|
|
613
|
+
else:
|
|
614
|
+
sandbox = self._modal.Sandbox.create(
|
|
615
|
+
app=self._app, workdir="/workspace", **client_kwargs
|
|
616
|
+
)
|
|
617
|
+
last_exc: Exception | None = None
|
|
618
|
+
for _ in range(timeout // 2):
|
|
619
|
+
if sandbox.poll() is not None:
|
|
620
|
+
msg = "Modal sandbox terminated unexpectedly during startup"
|
|
621
|
+
raise RuntimeError(msg)
|
|
622
|
+
try:
|
|
623
|
+
process = sandbox.exec("echo", "ready", timeout=5)
|
|
624
|
+
process.wait()
|
|
625
|
+
if process.returncode == 0:
|
|
626
|
+
break
|
|
627
|
+
except Exception as exc: # noqa: BLE001 # Transient failures expected during readiness polling
|
|
628
|
+
last_exc = exc
|
|
629
|
+
time.sleep(2)
|
|
630
|
+
else:
|
|
631
|
+
sandbox.terminate()
|
|
632
|
+
detail = f" Last error: {last_exc}" if last_exc else ""
|
|
633
|
+
msg = f"Modal sandbox failed to start within {timeout} seconds.{detail}"
|
|
634
|
+
raise RuntimeError(msg)
|
|
635
|
+
|
|
636
|
+
return modal_backend.ModalSandbox(sandbox=sandbox)
|
|
637
|
+
|
|
638
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
|
|
639
|
+
"""Terminate a Modal sandbox by id."""
|
|
640
|
+
del_kwargs: dict[str, Any] = {"sandbox_id": sandbox_id, "app": self._app}
|
|
641
|
+
if self._client is not None:
|
|
642
|
+
del_kwargs["client"] = self._client
|
|
643
|
+
sandbox = self._modal.Sandbox.from_id(**del_kwargs)
|
|
644
|
+
sandbox.terminate()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class _RunloopProvider(SandboxProvider):
|
|
648
|
+
"""Runloop sandbox provider — delegates to `langchain_runloop.RunloopProvider`."""
|
|
649
|
+
|
|
650
|
+
def __init__(self) -> None:
|
|
651
|
+
runloop_module = _import_provider_module(
|
|
652
|
+
"langchain_runloop",
|
|
653
|
+
provider="runloop",
|
|
654
|
+
package="langchain-runloop",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
from deepagents_code.model_config import resolve_env_var
|
|
658
|
+
|
|
659
|
+
api_key = resolve_env_var("RUNLOOP_API_KEY")
|
|
660
|
+
if not api_key:
|
|
661
|
+
msg = (
|
|
662
|
+
"No Runloop API key found. Set RUNLOOP_API_KEY "
|
|
663
|
+
"or DEEPAGENTS_CODE_RUNLOOP_API_KEY."
|
|
664
|
+
)
|
|
665
|
+
raise ValueError(msg)
|
|
666
|
+
self._provider = runloop_module.RunloopProvider(
|
|
667
|
+
api_key=api_key,
|
|
668
|
+
resolve_env_var=resolve_env_var,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def get_or_create(
|
|
672
|
+
self,
|
|
673
|
+
*,
|
|
674
|
+
sandbox_id: str | None = None,
|
|
675
|
+
timeout: int = 180,
|
|
676
|
+
**kwargs: Any,
|
|
677
|
+
) -> SandboxBackendProtocol:
|
|
678
|
+
"""Get or create a Runloop devbox.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
sandbox_id: Existing devbox ID, or None to create.
|
|
682
|
+
timeout: Accepted for parity with other providers; currently
|
|
683
|
+
forwarded but unused by the Runloop backend (the SDK manages
|
|
684
|
+
its own startup wait).
|
|
685
|
+
**kwargs: Runloop-specific options (`snapshot` blueprint name,
|
|
686
|
+
`blueprint_dockerfile`).
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
`RunloopSandbox` instance.
|
|
690
|
+
|
|
691
|
+
Raises:
|
|
692
|
+
SandboxNotFoundError: If `sandbox_id` does not exist. `RunloopProvider`
|
|
693
|
+
translates the SDK's not-found error into a `KeyError`, which is
|
|
694
|
+
mapped here.
|
|
695
|
+
KeyError: If a `KeyError` is raised while no `sandbox_id` was supplied
|
|
696
|
+
(re-raised unchanged rather than mislabeled as not-found).
|
|
697
|
+
"""
|
|
698
|
+
try:
|
|
699
|
+
return self._provider.get_or_create(
|
|
700
|
+
sandbox_id=sandbox_id,
|
|
701
|
+
timeout=timeout,
|
|
702
|
+
**kwargs,
|
|
703
|
+
)
|
|
704
|
+
except KeyError as e:
|
|
705
|
+
if sandbox_id is None:
|
|
706
|
+
raise
|
|
707
|
+
raise SandboxNotFoundError(sandbox_id) from e
|
|
708
|
+
|
|
709
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
|
|
710
|
+
"""Shut down a Runloop devbox by id."""
|
|
711
|
+
self._provider.delete(sandbox_id=sandbox_id)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class _AgentCoreProvider(SandboxProvider):
|
|
715
|
+
"""AgentCore Code Interpreter sandbox provider.
|
|
716
|
+
|
|
717
|
+
Manages AgentCore session lifecycle. Sessions cannot be reconnected after
|
|
718
|
+
the app exits — the `sandbox_id` parameter is not supported.
|
|
719
|
+
"""
|
|
720
|
+
|
|
721
|
+
def __init__(self, region: str | None = None) -> None:
|
|
722
|
+
"""Initialize AgentCore provider.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
region: AWS region (defaults to `AWS_REGION` /
|
|
726
|
+
`AWS_DEFAULT_REGION` / `us-west-2`).
|
|
727
|
+
|
|
728
|
+
Raises:
|
|
729
|
+
ValueError: If boto3 is installed and AWS credentials cannot
|
|
730
|
+
be resolved.
|
|
731
|
+
"""
|
|
732
|
+
self._region = region or os.environ.get(
|
|
733
|
+
"AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-west-2")
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
# Validate AWS credentials early for a clear error message.
|
|
737
|
+
try:
|
|
738
|
+
import boto3 # ty: ignore[unresolved-import]
|
|
739
|
+
|
|
740
|
+
session = boto3.Session()
|
|
741
|
+
credentials = session.get_credentials()
|
|
742
|
+
if credentials is None:
|
|
743
|
+
msg = (
|
|
744
|
+
"AWS credentials not found. Configure via "
|
|
745
|
+
"AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN, "
|
|
746
|
+
"~/.aws/credentials, or an IAM role."
|
|
747
|
+
)
|
|
748
|
+
raise ValueError(msg) # noqa: TRY301 # intentional raise for early credential validation
|
|
749
|
+
except ImportError:
|
|
750
|
+
logger.debug("boto3 not installed; skipping credential pre-check")
|
|
751
|
+
except ValueError:
|
|
752
|
+
raise
|
|
753
|
+
except Exception:
|
|
754
|
+
logger.warning(
|
|
755
|
+
"AWS credential pre-validation failed — the session may "
|
|
756
|
+
"fail to start. Check your AWS configuration.",
|
|
757
|
+
exc_info=True,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
self._active_interpreters: dict[str, Any] = {}
|
|
761
|
+
|
|
762
|
+
def get_or_create(
|
|
763
|
+
self,
|
|
764
|
+
*,
|
|
765
|
+
sandbox_id: str | None = None,
|
|
766
|
+
**kwargs: Any, # noqa: ARG002 # required by SandboxProvider interface
|
|
767
|
+
) -> SandboxBackendProtocol:
|
|
768
|
+
"""Create a new AgentCore Code Interpreter session.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
sandbox_id: Not supported — raises `NotImplementedError`
|
|
772
|
+
if provided.
|
|
773
|
+
**kwargs: Additional parameters (unused).
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
`AgentCoreSandbox` instance wrapping the started interpreter.
|
|
777
|
+
|
|
778
|
+
Raises:
|
|
779
|
+
NotImplementedError: If `sandbox_id` is provided.
|
|
780
|
+
"""
|
|
781
|
+
if sandbox_id:
|
|
782
|
+
msg = (
|
|
783
|
+
"AgentCore does not support reconnecting to existing sessions. "
|
|
784
|
+
"Remove the --sandbox-id option."
|
|
785
|
+
)
|
|
786
|
+
raise NotImplementedError(msg)
|
|
787
|
+
|
|
788
|
+
agentcore_module = _import_provider_module(
|
|
789
|
+
"bedrock_agentcore.tools.code_interpreter_client",
|
|
790
|
+
provider="agentcore",
|
|
791
|
+
package="langchain-agentcore-codeinterpreter",
|
|
792
|
+
)
|
|
793
|
+
agentcore_backend = _import_provider_module(
|
|
794
|
+
"langchain_agentcore_codeinterpreter",
|
|
795
|
+
provider="agentcore",
|
|
796
|
+
package="langchain-agentcore-codeinterpreter",
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
interpreter = agentcore_module.CodeInterpreter(
|
|
800
|
+
region=self._region,
|
|
801
|
+
integration_source="deepagents-code",
|
|
802
|
+
)
|
|
803
|
+
try:
|
|
804
|
+
interpreter.start()
|
|
805
|
+
except Exception:
|
|
806
|
+
with contextlib.suppress(Exception):
|
|
807
|
+
interpreter.stop()
|
|
808
|
+
raise
|
|
809
|
+
|
|
810
|
+
backend = agentcore_backend.AgentCoreSandbox(interpreter=interpreter)
|
|
811
|
+
self._active_interpreters[backend.id] = interpreter
|
|
812
|
+
return backend
|
|
813
|
+
|
|
814
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # required by SandboxProvider interface
|
|
815
|
+
"""Stop an AgentCore session.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
sandbox_id: Session ID to stop.
|
|
819
|
+
**kwargs: Additional parameters (unused).
|
|
820
|
+
"""
|
|
821
|
+
interpreter = self._active_interpreters.pop(sandbox_id, None)
|
|
822
|
+
if interpreter:
|
|
823
|
+
try:
|
|
824
|
+
interpreter.stop()
|
|
825
|
+
logger.info("AgentCore session %s stopped", sandbox_id)
|
|
826
|
+
except Exception:
|
|
827
|
+
logger.warning(
|
|
828
|
+
"Failed to stop AgentCore session %s — the session may "
|
|
829
|
+
"still be running and incurring costs. Check the AWS "
|
|
830
|
+
"console to verify.",
|
|
831
|
+
sandbox_id,
|
|
832
|
+
exc_info=True,
|
|
833
|
+
)
|
|
834
|
+
else:
|
|
835
|
+
logger.info(
|
|
836
|
+
"AgentCore session %s not tracked (may have already expired)",
|
|
837
|
+
sandbox_id,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
_VercelStatus = Literal[
|
|
842
|
+
"pending",
|
|
843
|
+
"running",
|
|
844
|
+
"stopping",
|
|
845
|
+
"stopped",
|
|
846
|
+
"failed",
|
|
847
|
+
"aborted",
|
|
848
|
+
"snapshotting",
|
|
849
|
+
]
|
|
850
|
+
"""Vercel sandbox lifecycle statuses (mirrors the SDK's `SandboxStatus` enum)."""
|
|
851
|
+
|
|
852
|
+
_VERCEL_TERMINAL_STATUSES: frozenset[_VercelStatus] = frozenset(
|
|
853
|
+
{"aborted", "failed", "stopped"}
|
|
854
|
+
)
|
|
855
|
+
"""Vercel sandbox statuses that cannot become ready without a new sandbox.
|
|
856
|
+
|
|
857
|
+
Typing the members as `_VercelStatus` turns a typo into a type error rather than
|
|
858
|
+
a silently-never-matching string.
|
|
859
|
+
"""
|
|
860
|
+
|
|
861
|
+
_VERCEL_DEFAULT_RUNTIME = "python3.13"
|
|
862
|
+
"""Runtime used when creating a fresh Vercel sandbox."""
|
|
863
|
+
|
|
864
|
+
_VERCEL_SANDBOX_TIMEOUT = timedelta(minutes=30)
|
|
865
|
+
"""Lifetime used for freshly created Vercel sandboxes."""
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class _VercelSandboxHandle(Protocol):
|
|
869
|
+
"""Minimal Vercel SDK sandbox surface used by the built-in provider."""
|
|
870
|
+
|
|
871
|
+
sandbox_id: str
|
|
872
|
+
status: str
|
|
873
|
+
|
|
874
|
+
def wait_for_status(self, status: str, *, timeout: int) -> None:
|
|
875
|
+
"""Wait for the sandbox to reach the requested status."""
|
|
876
|
+
|
|
877
|
+
def stop(self) -> None:
|
|
878
|
+
"""Stop the sandbox."""
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class _VercelProvider(SandboxProvider):
|
|
882
|
+
"""Vercel Sandbox provider implementation."""
|
|
883
|
+
|
|
884
|
+
_CREDENTIAL_ENV_NAMES = (
|
|
885
|
+
"VERCEL_TOKEN",
|
|
886
|
+
"VERCEL_PROJECT_ID",
|
|
887
|
+
"VERCEL_TEAM_ID",
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
def __init__(self) -> None:
|
|
891
|
+
self._sdk_kwargs = self._resolve_sdk_kwargs()
|
|
892
|
+
|
|
893
|
+
@classmethod
|
|
894
|
+
def _resolve_sdk_kwargs(cls) -> dict[str, str]:
|
|
895
|
+
"""Resolve explicit Vercel credentials when a prefixed override is set.
|
|
896
|
+
|
|
897
|
+
Credentials stay SDK-managed (canonical `VERCEL_*` variables or OIDC)
|
|
898
|
+
unless at least one of the prefixed overrides
|
|
899
|
+
`DEEPAGENTS_CODE_VERCEL_TOKEN`, `DEEPAGENTS_CODE_VERCEL_PROJECT_ID`, or
|
|
900
|
+
`DEEPAGENTS_CODE_VERCEL_TEAM_ID` is present. When triggered, each value
|
|
901
|
+
still resolves prefixed-first then canonical via `resolve_env_var`.
|
|
902
|
+
|
|
903
|
+
Returns:
|
|
904
|
+
Explicit SDK credential arguments, or an empty mapping to delegate
|
|
905
|
+
credential resolution to the Vercel SDK.
|
|
906
|
+
"""
|
|
907
|
+
prefix = "DEEPAGENTS_CODE_"
|
|
908
|
+
has_override = any(
|
|
909
|
+
f"{prefix}{name}" in os.environ for name in cls._CREDENTIAL_ENV_NAMES
|
|
910
|
+
)
|
|
911
|
+
if not has_override:
|
|
912
|
+
return {}
|
|
913
|
+
|
|
914
|
+
from deepagents_code.model_config import resolve_env_var
|
|
915
|
+
|
|
916
|
+
values = {
|
|
917
|
+
"token": resolve_env_var("VERCEL_TOKEN"),
|
|
918
|
+
"project_id": resolve_env_var("VERCEL_PROJECT_ID"),
|
|
919
|
+
"team_id": resolve_env_var("VERCEL_TEAM_ID"),
|
|
920
|
+
}
|
|
921
|
+
missing = sorted(key for key, value in values.items() if not value)
|
|
922
|
+
if missing:
|
|
923
|
+
logger.warning(
|
|
924
|
+
"Incomplete explicit Vercel credentials; VERCEL_TOKEN, "
|
|
925
|
+
"VERCEL_PROJECT_ID, and VERCEL_TEAM_ID are all required. "
|
|
926
|
+
"Falling back to default Vercel authentication. Missing: %s",
|
|
927
|
+
", ".join(missing),
|
|
928
|
+
)
|
|
929
|
+
return {}
|
|
930
|
+
return {key: value for key, value in values.items() if value}
|
|
931
|
+
|
|
932
|
+
def get_or_create(
|
|
933
|
+
self,
|
|
934
|
+
*,
|
|
935
|
+
sandbox_id: str | None = None,
|
|
936
|
+
timeout: int = 180,
|
|
937
|
+
**kwargs: Any,
|
|
938
|
+
) -> SandboxBackendProtocol:
|
|
939
|
+
"""Get or create a Vercel sandbox.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
sandbox_id: Existing sandbox ID, or None to create.
|
|
943
|
+
timeout: Seconds to wait for startup.
|
|
944
|
+
**kwargs: Rejected; passing any keyword argument raises `TypeError`.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
`VercelSandbox` instance.
|
|
948
|
+
|
|
949
|
+
Raises:
|
|
950
|
+
RuntimeError: If the sandbox fails to start.
|
|
951
|
+
TypeError: If unsupported keyword arguments are provided.
|
|
952
|
+
"""
|
|
953
|
+
if kwargs:
|
|
954
|
+
msg = f"Received unsupported arguments: {list(kwargs.keys())}"
|
|
955
|
+
raise TypeError(msg)
|
|
956
|
+
|
|
957
|
+
vercel_backend = _import_provider_module(
|
|
958
|
+
"langchain_vercel_sandbox",
|
|
959
|
+
provider="vercel",
|
|
960
|
+
package="langchain-vercel-sandbox",
|
|
961
|
+
)
|
|
962
|
+
vercel_sandbox = _import_provider_module(
|
|
963
|
+
"vercel.sandbox",
|
|
964
|
+
provider="vercel",
|
|
965
|
+
package="vercel",
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
# The SDK is imported dynamically, so `Sandbox.get`/`create` are typed
|
|
969
|
+
# `Any`. Annotating pins the result to the Protocol so the type checker
|
|
970
|
+
# verifies the `.status`/`.wait_for_status`/`.stop` calls below.
|
|
971
|
+
sandbox: _VercelSandboxHandle
|
|
972
|
+
try:
|
|
973
|
+
if sandbox_id:
|
|
974
|
+
sandbox = vercel_sandbox.Sandbox.get(
|
|
975
|
+
sandbox_id=sandbox_id,
|
|
976
|
+
**self._sdk_kwargs,
|
|
977
|
+
)
|
|
978
|
+
else:
|
|
979
|
+
sandbox = vercel_sandbox.Sandbox.create(
|
|
980
|
+
runtime=_VERCEL_DEFAULT_RUNTIME,
|
|
981
|
+
timeout=_VERCEL_SANDBOX_TIMEOUT,
|
|
982
|
+
**self._sdk_kwargs,
|
|
983
|
+
)
|
|
984
|
+
except Exception as exc: # Vercel SDK exception types vary by version
|
|
985
|
+
# Keep the message generic so SDK errors cannot leak credentials,
|
|
986
|
+
# but chain the original exception so tracebacks retain root cause.
|
|
987
|
+
action = "connect to existing" if sandbox_id else "create"
|
|
988
|
+
msg = f"Failed to {action} Vercel sandbox."
|
|
989
|
+
raise RuntimeError(msg) from exc
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
self._wait_until_running(sandbox, timeout=timeout)
|
|
993
|
+
except Exception:
|
|
994
|
+
if sandbox_id is None:
|
|
995
|
+
with contextlib.suppress(Exception):
|
|
996
|
+
sandbox.stop()
|
|
997
|
+
raise
|
|
998
|
+
|
|
999
|
+
return vercel_backend.VercelSandbox(sandbox=sandbox)
|
|
1000
|
+
|
|
1001
|
+
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # **kwargs required by SandboxProvider interface
|
|
1002
|
+
"""Stop a Vercel sandbox by id.
|
|
1003
|
+
|
|
1004
|
+
Raises:
|
|
1005
|
+
RuntimeError: If the Vercel SDK cannot find or stop the sandbox.
|
|
1006
|
+
"""
|
|
1007
|
+
vercel_sandbox = _import_provider_module(
|
|
1008
|
+
"vercel.sandbox",
|
|
1009
|
+
provider="vercel",
|
|
1010
|
+
package="vercel",
|
|
1011
|
+
)
|
|
1012
|
+
try:
|
|
1013
|
+
sandbox = vercel_sandbox.Sandbox.get(
|
|
1014
|
+
sandbox_id=sandbox_id,
|
|
1015
|
+
**self._sdk_kwargs,
|
|
1016
|
+
)
|
|
1017
|
+
sandbox.stop()
|
|
1018
|
+
except Exception as exc: # Vercel SDK exception types vary by version
|
|
1019
|
+
# Generic message avoids leaking credentials; chain preserves cause.
|
|
1020
|
+
msg = "Failed to stop Vercel sandbox."
|
|
1021
|
+
raise RuntimeError(msg) from exc
|
|
1022
|
+
|
|
1023
|
+
@staticmethod
|
|
1024
|
+
def _wait_until_running(
|
|
1025
|
+
sandbox: _VercelSandboxHandle,
|
|
1026
|
+
*,
|
|
1027
|
+
timeout: int,
|
|
1028
|
+
) -> None:
|
|
1029
|
+
"""Wait for a Vercel sandbox to become ready.
|
|
1030
|
+
|
|
1031
|
+
Raises:
|
|
1032
|
+
RuntimeError: If the sandbox reaches a terminal state or does not
|
|
1033
|
+
become ready before the timeout.
|
|
1034
|
+
"""
|
|
1035
|
+
status = str(sandbox.status)
|
|
1036
|
+
if status == "running":
|
|
1037
|
+
return
|
|
1038
|
+
if status in _VERCEL_TERMINAL_STATUSES:
|
|
1039
|
+
msg = f"Vercel sandbox {sandbox.sandbox_id} is in terminal state {status!r}"
|
|
1040
|
+
raise RuntimeError(msg)
|
|
1041
|
+
|
|
1042
|
+
try:
|
|
1043
|
+
sandbox.wait_for_status("running", timeout=timeout)
|
|
1044
|
+
except TimeoutError as exc:
|
|
1045
|
+
status = str(sandbox.status)
|
|
1046
|
+
msg = (
|
|
1047
|
+
f"Vercel sandbox {sandbox.sandbox_id} failed to start within "
|
|
1048
|
+
f"{timeout} seconds; current status is {status!r}"
|
|
1049
|
+
)
|
|
1050
|
+
raise RuntimeError(msg) from exc
|
|
1051
|
+
except Exception as exc: # Vercel SDK exception types vary by version
|
|
1052
|
+
# Generic message avoids leaking credentials; chain preserves cause.
|
|
1053
|
+
msg = "Failed while waiting for Vercel sandbox startup."
|
|
1054
|
+
raise RuntimeError(msg) from exc
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _get_provider(
|
|
1058
|
+
provider_name: str,
|
|
1059
|
+
registry: SandboxRegistry | None = None,
|
|
1060
|
+
) -> SandboxProvider:
|
|
1061
|
+
"""Get a `SandboxProvider` instance for the specified provider (internal).
|
|
1062
|
+
|
|
1063
|
+
Args:
|
|
1064
|
+
provider_name: Name of the provider. Resolved through the registry so
|
|
1065
|
+
built-in, entry-point, and config providers are all supported.
|
|
1066
|
+
registry: An already-built registry to reuse. A fresh one is loaded
|
|
1067
|
+
when omitted.
|
|
1068
|
+
|
|
1069
|
+
Returns:
|
|
1070
|
+
`SandboxProvider` instance. Propagates `ValueError` from the registry
|
|
1071
|
+
if `provider_name` is unknown.
|
|
1072
|
+
"""
|
|
1073
|
+
reg = registry if registry is not None else _get_registry()
|
|
1074
|
+
return reg.create_provider(provider_name)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def verify_sandbox_deps(provider: str) -> None:
|
|
1078
|
+
"""Check that the required packages for a sandbox provider are installed.
|
|
1079
|
+
|
|
1080
|
+
Uses `importlib.util.find_spec` for a lightweight check with no actual
|
|
1081
|
+
imports. Call this in the app's process *before* spawning the server
|
|
1082
|
+
subprocess so users get a clear, actionable error instead of an opaque
|
|
1083
|
+
server crash. The backend module to probe and the install hint both come
|
|
1084
|
+
from provider metadata.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
provider: Sandbox provider name (e.g. `'daytona'`).
|
|
1088
|
+
|
|
1089
|
+
Raises:
|
|
1090
|
+
ImportError: If the provider's backend package is not installed.
|
|
1091
|
+
"""
|
|
1092
|
+
if not provider or provider == "none":
|
|
1093
|
+
return
|
|
1094
|
+
|
|
1095
|
+
metadata = _get_registry().get_metadata(provider)
|
|
1096
|
+
if metadata is None or metadata.backend_module is None:
|
|
1097
|
+
logger.debug(
|
|
1098
|
+
"No backend module to probe for provider %r; skipping pre-flight check",
|
|
1099
|
+
provider,
|
|
1100
|
+
)
|
|
1101
|
+
return
|
|
1102
|
+
|
|
1103
|
+
try:
|
|
1104
|
+
found = importlib.util.find_spec(metadata.backend_module) is not None
|
|
1105
|
+
except (ImportError, ValueError):
|
|
1106
|
+
found = False
|
|
1107
|
+
|
|
1108
|
+
if not found:
|
|
1109
|
+
if metadata.install is not None:
|
|
1110
|
+
install_hint = (
|
|
1111
|
+
f"Install with: {metadata.install.command(in_app=True)} (in-app) "
|
|
1112
|
+
f"or {metadata.install.command(in_app=False)} (CLI)"
|
|
1113
|
+
)
|
|
1114
|
+
else:
|
|
1115
|
+
install_hint = "Install the provider's package."
|
|
1116
|
+
msg = f"Missing dependencies for '{provider}' sandbox. {install_hint}"
|
|
1117
|
+
raise ImportError(msg)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
__all__ = [
|
|
1121
|
+
"create_sandbox",
|
|
1122
|
+
"get_default_working_dir",
|
|
1123
|
+
"verify_sandbox_deps",
|
|
1124
|
+
]
|