langchain-e2b 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (27) hide show
  1. langchain_e2b-0.0.4/CHANGELOG.md +21 -0
  2. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/PKG-INFO +31 -13
  3. langchain_e2b-0.0.4/README.md +71 -0
  4. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/langchain_e2b/__init__.py +1 -2
  5. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/langchain_e2b/_version.py +1 -1
  6. langchain_e2b-0.0.4/langchain_e2b/provider.py +215 -0
  7. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/pyproject.toml +5 -1
  8. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/integration_tests/test_integration.py +42 -0
  9. langchain_e2b-0.0.4/tests/unit_tests/test_import.py +13 -0
  10. langchain_e2b-0.0.4/tests/unit_tests/test_provider.py +310 -0
  11. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/uv.lock +1698 -57
  12. langchain_e2b-0.0.2/CHANGELOG.md +0 -7
  13. langchain_e2b-0.0.2/README.md +0 -53
  14. langchain_e2b-0.0.2/langchain_e2b/provider.py +0 -136
  15. langchain_e2b-0.0.2/tests/unit_tests/test_import.py +0 -7
  16. langchain_e2b-0.0.2/tests/unit_tests/test_provider.py +0 -159
  17. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/.github/workflows/ci.yml +0 -0
  18. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/.github/workflows/release.yml +0 -0
  19. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/.gitignore +0 -0
  20. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/LICENSE +0 -0
  21. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/Makefile +0 -0
  22. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/langchain_e2b/sandbox.py +0 -0
  23. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/__init__.py +0 -0
  24. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/integration_tests/__init__.py +0 -0
  25. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/test_import.py +0 -0
  26. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/unit_tests/__init__.py +0 -0
  27. {langchain_e2b-0.0.2 → langchain_e2b-0.0.4}/tests/unit_tests/test_sandbox.py +0 -0
@@ -0,0 +1,21 @@
1
+ <!-- markdownlint-disable MD024 -->
2
+
3
+ # Changelog
4
+
5
+ ## 0.0.4
6
+
7
+ - Added a Deep Agents Code sandbox provider entry point so `dcode --sandbox e2b`
8
+ works after installing `langchain-e2b` into the `dcode` environment.
9
+
10
+ ## 0.0.3
11
+
12
+ - Removed the `E2BProvider` lifecycle helper. Use the E2B SDK to create,
13
+ configure, connect to, and kill sandboxes, then wrap them with `E2BSandbox`.
14
+
15
+ ## 0.0.2
16
+
17
+ - Added the `E2BProvider` lifecycle helper.
18
+
19
+ ## 0.0.1
20
+
21
+ - Initial E2B sandbox integration for Deep Agents.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-e2b
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: E2B sandbox integration for Deep Agents
5
5
  Project-URL: Homepage, https://github.com/e2b-dev/langchain-e2b
6
6
  Project-URL: Repository, https://github.com/e2b-dev/langchain-e2b
@@ -33,33 +33,51 @@ Description-Content-Type: text/markdown
33
33
  pip install langchain-e2b
34
34
  ```
35
35
 
36
+ ## Deep Agents SDK
37
+
36
38
  ```python
37
- from langchain_e2b import E2BProvider
39
+ from e2b import Sandbox
40
+ from langchain_e2b import E2BSandbox
38
41
 
39
- provider = E2BProvider()
40
- sandbox = provider.get_or_create()
42
+ e2b_sandbox = Sandbox.create()
43
+ backend = E2BSandbox(sandbox=e2b_sandbox)
41
44
 
42
45
  try:
43
- result = sandbox.execute("echo hello")
46
+ result = backend.execute("echo hello")
44
47
  print(result.output)
45
48
  finally:
46
- provider.delete(sandbox_id=sandbox.id)
49
+ e2b_sandbox.kill()
50
+ ```
51
+
52
+ ## Deep Agents Code
53
+
54
+ Install `langchain-e2b` into the `dcode` environment, then run with the E2B
55
+ sandbox provider:
56
+
57
+ ```bash
58
+ dcode --install langchain-e2b --package
59
+ export E2B_API_KEY=...
60
+ dcode --sandbox e2b
47
61
  ```
48
62
 
49
63
  ## What is this?
50
64
 
51
- `langchain-e2b` adapts E2B sandboxes to the Deep Agents sandbox protocol. It
52
- uses the low-level `e2b` SDK so Deep Agents can create or reconnect to
53
- sandboxes, run shell commands, and move files through the standard Deep Agents
54
- sandbox interface.
65
+ `langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
66
+ protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
67
+ and move files through the standard Deep Agents sandbox interface.
68
+
69
+ For SDK use, this package intentionally does not hide E2B sandbox lifecycle
70
+ management. Use the E2B SDK to create, connect to, configure, and kill
71
+ sandboxes, then pass the connected sandbox object to `E2BSandbox`.
55
72
 
56
- Use `E2BProvider` when you want the package to manage sandbox lifecycle. Use
57
- `E2BSandbox` directly when you already have an E2B SDK sandbox object.
73
+ For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
74
+ point. The provider creates, reconnects to, and deletes E2B sandboxes for
75
+ `dcode --sandbox e2b`.
58
76
 
59
77
  ## Contributing
60
78
 
61
79
  Contributions are welcome. Keep the adapter focused on implementing the Deep
62
- Agents sandbox protocol over the official E2B SDK.
80
+ Agents sandbox backend protocol over the official E2B SDK.
63
81
 
64
82
  ## Development
65
83
 
@@ -0,0 +1,71 @@
1
+ # langchain-e2b
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/langchain-e2b?label=%20)](https://pypi.org/project/langchain-e2b/#history)
4
+ [![PyPI - License](https://img.shields.io/pypi/l/langchain-e2b)](https://opensource.org/licenses/MIT)
5
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/langchain-e2b)](https://pypistats.org/packages/langchain-e2b)
6
+
7
+ ## Quick Install
8
+
9
+ ```bash
10
+ pip install langchain-e2b
11
+ ```
12
+
13
+ ## Deep Agents SDK
14
+
15
+ ```python
16
+ from e2b import Sandbox
17
+ from langchain_e2b import E2BSandbox
18
+
19
+ e2b_sandbox = Sandbox.create()
20
+ backend = E2BSandbox(sandbox=e2b_sandbox)
21
+
22
+ try:
23
+ result = backend.execute("echo hello")
24
+ print(result.output)
25
+ finally:
26
+ e2b_sandbox.kill()
27
+ ```
28
+
29
+ ## Deep Agents Code
30
+
31
+ Install `langchain-e2b` into the `dcode` environment, then run with the E2B
32
+ sandbox provider:
33
+
34
+ ```bash
35
+ dcode --install langchain-e2b --package
36
+ export E2B_API_KEY=...
37
+ dcode --sandbox e2b
38
+ ```
39
+
40
+ ## What is this?
41
+
42
+ `langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
43
+ protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
44
+ and move files through the standard Deep Agents sandbox interface.
45
+
46
+ For SDK use, this package intentionally does not hide E2B sandbox lifecycle
47
+ management. Use the E2B SDK to create, connect to, configure, and kill
48
+ sandboxes, then pass the connected sandbox object to `E2BSandbox`.
49
+
50
+ For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
51
+ point. The provider creates, reconnects to, and deletes E2B sandboxes for
52
+ `dcode --sandbox e2b`.
53
+
54
+ ## Contributing
55
+
56
+ Contributions are welcome. Keep the adapter focused on implementing the Deep
57
+ Agents sandbox backend protocol over the official E2B SDK.
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ uv sync --group test
63
+ make test
64
+ make lint
65
+ ```
66
+
67
+ Integration tests require `E2B_API_KEY`:
68
+
69
+ ```bash
70
+ E2B_API_KEY=... make integration_tests
71
+ ```
@@ -1,6 +1,5 @@
1
1
  """E2B sandbox integration for Deep Agents."""
2
2
 
3
- from langchain_e2b.provider import E2BProvider
4
3
  from langchain_e2b.sandbox import E2BSandbox
5
4
 
6
- __all__ = ["E2BProvider", "E2BSandbox"]
5
+ __all__ = ["E2BSandbox"]
@@ -2,4 +2,4 @@
2
2
 
3
3
  # Keep the `x-release-please-version` annotation — release-please uses it to
4
4
  # bump `__version__` in sync with `pyproject.toml` on every release PR.
5
- __version__ = "0.0.2" # x-release-please-version
5
+ __version__ = "0.0.4" # x-release-please-version
@@ -0,0 +1,215 @@
1
+ """E2B sandbox lifecycle provider for Deep Agents Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Callable
7
+ from typing import NoReturn
8
+
9
+ import e2b
10
+ from deepagents_code.integrations.sandbox_provider import (
11
+ SandboxInstallHint,
12
+ SandboxNotFoundError,
13
+ SandboxProvider,
14
+ SandboxProviderMetadata,
15
+ )
16
+
17
+ from langchain_e2b.sandbox import DEFAULT_WORKDIR, E2BSandbox
18
+
19
+ DEFAULT_SANDBOX_TIMEOUT = 30 * 60
20
+ DEFAULT_COMMAND_TIMEOUT = 30 * 60
21
+
22
+ EnvResolver = Callable[[str], str | None]
23
+
24
+
25
+ def _default_resolve_env_var(name: str) -> str | None:
26
+ if not name.startswith("DEEPAGENTS_CODE_"):
27
+ prefixed = f"DEEPAGENTS_CODE_{name}"
28
+ if prefixed in os.environ:
29
+ return os.environ[prefixed] or None
30
+ return os.environ.get(name) or None
31
+
32
+
33
+ def _resolve_optional_env_var(
34
+ resolve_env_var: EnvResolver,
35
+ name: str,
36
+ ) -> str | None:
37
+ value = resolve_env_var(name)
38
+ return value or None
39
+
40
+
41
+ def _resolve_int(
42
+ value: int | str | None,
43
+ *,
44
+ name: str,
45
+ default: int,
46
+ allow_zero: bool = False,
47
+ ) -> int:
48
+ if value is None:
49
+ return default
50
+
51
+ try:
52
+ resolved = int(value)
53
+ except (TypeError, ValueError) as exc:
54
+ msg = f"{name} must be an integer number of seconds"
55
+ raise ValueError(msg) from exc
56
+
57
+ lower_bound = 0 if allow_zero else 1
58
+ if resolved < lower_bound:
59
+ requirement = "non-negative" if allow_zero else "positive"
60
+ msg = f"{name} must be {requirement}"
61
+ raise ValueError(msg)
62
+ return resolved
63
+
64
+
65
+ def _raise_unsupported_kwargs(kwargs: dict[str, object]) -> NoReturn:
66
+ names = ", ".join(sorted(kwargs))
67
+ msg = f"Received unsupported arguments: {names}"
68
+ raise TypeError(msg)
69
+
70
+
71
+ class E2BProvider(SandboxProvider):
72
+ """Manage E2B sandboxes for Deep Agents Code."""
73
+
74
+ _metadata = SandboxProviderMetadata(
75
+ name="e2b",
76
+ working_dir=DEFAULT_WORKDIR,
77
+ install=SandboxInstallHint(kind="package", name="langchain-e2b"),
78
+ supports_sandbox_id=True,
79
+ supports_snapshot_name=False,
80
+ backend_module="langchain_e2b",
81
+ )
82
+
83
+ def __init__(
84
+ self,
85
+ *,
86
+ resolve_env_var: EnvResolver | None = None,
87
+ ) -> None:
88
+ """Initialize the provider without touching credentials.
89
+
90
+ Deep Agents Code may instantiate providers only to read metadata during
91
+ discovery, so credentials are resolved lazily by lifecycle methods.
92
+
93
+ Args:
94
+ resolve_env_var: Environment resolver used for provider settings.
95
+ """
96
+ self._resolve_env_var = resolve_env_var or _default_resolve_env_var
97
+
98
+ @property
99
+ def metadata(self) -> SandboxProviderMetadata:
100
+ """Return metadata used by Deep Agents Code provider discovery."""
101
+ return self._metadata
102
+
103
+ def get_or_create(
104
+ self,
105
+ *,
106
+ sandbox_id: str | None = None,
107
+ timeout: int | str | None = None,
108
+ template: str | None = None,
109
+ workdir: str = DEFAULT_WORKDIR,
110
+ command_timeout: int | str | None = None,
111
+ **kwargs: object,
112
+ ) -> E2BSandbox:
113
+ """Get or create an E2B sandbox backend.
114
+
115
+ Args:
116
+ sandbox_id: Existing E2B sandbox ID, or `None` to create one.
117
+ timeout: E2B sandbox lifetime in seconds.
118
+ template: E2B template for new sandboxes.
119
+ workdir: Working directory used by command execution.
120
+ command_timeout: Default command timeout for the backend.
121
+ **kwargs: Unsupported provider options.
122
+
123
+ Returns:
124
+ E2B sandbox backend.
125
+
126
+ Raises:
127
+ SandboxNotFoundError: If `sandbox_id` does not exist.
128
+ TypeError: If unsupported provider options are passed.
129
+ ValueError: If credentials or timeout settings are invalid.
130
+ """
131
+ if kwargs:
132
+ _raise_unsupported_kwargs(kwargs)
133
+
134
+ api_key = self._resolve_api_key()
135
+ sandbox_timeout = self._resolve_sandbox_timeout(timeout)
136
+ default_command_timeout = _resolve_int(
137
+ command_timeout,
138
+ name="command_timeout",
139
+ default=DEFAULT_COMMAND_TIMEOUT,
140
+ allow_zero=True,
141
+ )
142
+
143
+ try:
144
+ if sandbox_id is not None:
145
+ sandbox = e2b.Sandbox.connect(
146
+ sandbox_id,
147
+ timeout=sandbox_timeout,
148
+ api_key=api_key,
149
+ )
150
+ else:
151
+ resolved_template = template or _resolve_optional_env_var(
152
+ self._resolve_env_var,
153
+ "E2B_TEMPLATE",
154
+ )
155
+ if resolved_template:
156
+ sandbox = e2b.Sandbox.create(
157
+ template=resolved_template,
158
+ timeout=sandbox_timeout,
159
+ api_key=api_key,
160
+ )
161
+ else:
162
+ sandbox = e2b.Sandbox.create(
163
+ timeout=sandbox_timeout,
164
+ api_key=api_key,
165
+ )
166
+ except e2b.SandboxNotFoundException as exc:
167
+ if sandbox_id is None:
168
+ raise
169
+ raise SandboxNotFoundError(sandbox_id) from exc
170
+
171
+ return E2BSandbox(
172
+ sandbox=sandbox,
173
+ workdir=workdir,
174
+ timeout=default_command_timeout,
175
+ )
176
+
177
+ def delete(self, *, sandbox_id: str, **kwargs: object) -> None:
178
+ """Kill an E2B sandbox.
179
+
180
+ Args:
181
+ sandbox_id: E2B sandbox ID.
182
+ **kwargs: Unsupported provider options.
183
+
184
+ Raises:
185
+ SandboxNotFoundError: If `sandbox_id` does not exist.
186
+ TypeError: If unsupported provider options are passed.
187
+ ValueError: If credentials are missing.
188
+ """
189
+ if kwargs:
190
+ _raise_unsupported_kwargs(kwargs)
191
+ try:
192
+ e2b.Sandbox.kill(sandbox_id, api_key=self._resolve_api_key())
193
+ except e2b.SandboxNotFoundException as exc:
194
+ raise SandboxNotFoundError(sandbox_id) from exc
195
+
196
+ def _resolve_api_key(self) -> str:
197
+ api_key = _resolve_optional_env_var(self._resolve_env_var, "E2B_API_KEY")
198
+ if not api_key:
199
+ msg = (
200
+ "No E2B API key found. Set E2B_API_KEY or DEEPAGENTS_CODE_E2B_API_KEY."
201
+ )
202
+ raise ValueError(msg)
203
+ return api_key
204
+
205
+ def _resolve_sandbox_timeout(self, timeout: int | str | None) -> int:
206
+ env_timeout = (
207
+ None
208
+ if timeout is not None
209
+ else _resolve_optional_env_var(self._resolve_env_var, "E2B_SANDBOX_TIMEOUT")
210
+ )
211
+ return _resolve_int(
212
+ timeout if timeout is not None else env_timeout,
213
+ name="timeout",
214
+ default=DEFAULT_SANDBOX_TIMEOUT,
215
+ )
@@ -19,13 +19,16 @@ classifiers = [
19
19
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
20
  ]
21
21
 
22
- version = "0.0.2"
22
+ version = "0.0.4"
23
23
  requires-python = ">=3.11,<4.0"
24
24
  dependencies = [
25
25
  "deepagents>=0.6.0,<0.7.0",
26
26
  "e2b>=2.25.1,<3.0.0",
27
27
  ]
28
28
 
29
+ [project.entry-points."deepagents_code.sandbox_providers"]
30
+ e2b = "langchain_e2b.provider:E2BProvider"
31
+
29
32
  [tool.hatch.build.targets.wheel]
30
33
  packages = ["langchain_e2b"]
31
34
 
@@ -46,6 +49,7 @@ test = [
46
49
  "ruff>=0.13.1,<0.16.0",
47
50
  "ty>=0.0.1,<1.0.0",
48
51
  "langchain-tests>=1.1.6",
52
+ "deepagents-code>=0.1.19,<0.2.0",
49
53
  ]
50
54
 
51
55
  [tool.uv]
@@ -11,6 +11,7 @@ from e2b.exceptions import SandboxException
11
11
  from langchain_tests.integration_tests import SandboxIntegrationTests
12
12
 
13
13
  from langchain_e2b import E2BSandbox
14
+ from langchain_e2b.provider import E2BProvider
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from collections.abc import Iterator
@@ -47,6 +48,31 @@ class TestE2BSandboxStandard(SandboxIntegrationTests):
47
48
  _kill_sandbox(sandbox)
48
49
 
49
50
 
51
+ def test_e2b_provider_creates_executes_and_deletes_sandbox() -> None:
52
+ api_key = os.environ.get("E2B_API_KEY")
53
+ if not api_key:
54
+ pytest.skip(
55
+ "Missing secrets for E2B provider integration test: set E2B_API_KEY"
56
+ )
57
+
58
+ provider = E2BProvider()
59
+ sandbox_id: str | None = None
60
+ try:
61
+ backend = provider.get_or_create(
62
+ timeout=60 * 60,
63
+ template=os.environ.get("E2B_TEMPLATE"),
64
+ command_timeout=30,
65
+ )
66
+ sandbox_id = backend.id
67
+ result = backend.execute("echo provider-ready", timeout=10)
68
+
69
+ assert result.exit_code == 0
70
+ assert "provider-ready" in result.output
71
+ finally:
72
+ if sandbox_id is not None:
73
+ _delete_provider_sandbox(provider, sandbox_id)
74
+
75
+
50
76
  def _kill_sandbox(sandbox: Sandbox) -> None:
51
77
  last_error: BaseException | None = None
52
78
  for attempt in range(KILL_ATTEMPTS):
@@ -61,3 +87,19 @@ def _kill_sandbox(sandbox: Sandbox) -> None:
61
87
 
62
88
  msg = f"Failed to kill E2B sandbox {sandbox.sandbox_id!r}"
63
89
  raise RuntimeError(msg) from last_error
90
+
91
+
92
+ def _delete_provider_sandbox(provider: E2BProvider, sandbox_id: str) -> None:
93
+ last_error: BaseException | None = None
94
+ for attempt in range(KILL_ATTEMPTS):
95
+ try:
96
+ provider.delete(sandbox_id=sandbox_id)
97
+ except (httpx.HTTPError, SandboxException) as exc:
98
+ last_error = exc
99
+ if attempt + 1 < KILL_ATTEMPTS:
100
+ time.sleep(KILL_RETRY_DELAY_SECONDS)
101
+ else:
102
+ return
103
+
104
+ msg = f"Failed to delete E2B sandbox {sandbox_id!r}"
105
+ raise RuntimeError(msg) from last_error
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ import langchain_e2b
4
+
5
+
6
+ def test_import_e2b() -> None:
7
+ assert langchain_e2b is not None
8
+
9
+
10
+ def test_public_exports_only_sandbox() -> None:
11
+ assert langchain_e2b.__all__ == ["E2BSandbox"]
12
+ assert hasattr(langchain_e2b, "E2BSandbox")
13
+ assert not hasattr(langchain_e2b, "E2BProvider")