Repository navigation
feat(sandbox): support AgentENV auto-pause and resume #3757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3bc7882
2710057
743605c
23a3e5b
648df58
050b939
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -184,9 +184,12 @@ class E2BCreateConfig: | |||||||||||||||||
| # image references are not themselves valid aliases (registry refs contain | ||||||||||||||||||
| # '/' and ':', which E2B rejects). | ||||||||||||||||||
| template_map: dict[str, str] = field(default_factory=dict) | ||||||||||||||||||
| # Sandbox lifetime in seconds; E2B kills the sandbox when it elapses. | ||||||||||||||||||
| # Sandbox timeout in seconds; the lifecycle policy selects pause or kill. | ||||||||||||||||||
| # ``SandboxSpec.ttl_s`` overrides it per sandbox. | ||||||||||||||||||
| timeout_s: float | None = 3600.0 | ||||||||||||||||||
| # True pauses/resumes; False kills at timeout; None sends no lifecycle. E2B 2.42+ then leaves | ||||||||||||||||||
| # the choice to the server, and AgentENV pauses without resume and never evicts the sandbox. | ||||||||||||||||||
| auto_resume: bool | None = field(default=False, kw_only=True) | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that
Either way, the 2.36–2.41 history here, in |
||||||||||||||||||
| allow_internet_access: bool = True | ||||||||||||||||||
| secure: bool = True | ||||||||||||||||||
| # Outbound traffic rules, passed as the SDK's ``network`` option: | ||||||||||||||||||
|
|
@@ -225,6 +228,8 @@ def __post_init__(self) -> None: | |||||||||||||||||
| raise ValueError(f"create.network.{key} must be a list of strings") | ||||||||||||||||||
| if self.custom_extension_params is not None and not isinstance(self.custom_extension_params, Mapping): | ||||||||||||||||||
| raise TypeError("create.custom_extension_params must be a mapping") | ||||||||||||||||||
| if self.auto_resume is not None and not isinstance(self.auto_resume, bool): | ||||||||||||||||||
| raise ValueError("create.auto_resume must be a boolean or None") | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @dataclass(frozen=True) | ||||||||||||||||||
|
|
@@ -304,13 +309,7 @@ def _api_params(self) -> dict[str, Any]: | |||||||||||||||||
| return params | ||||||||||||||||||
|
|
||||||||||||||||||
| def _request_params(self) -> dict[str, Any]: | ||||||||||||||||||
| """Per-request options for calls on an existing sandbox object. | ||||||||||||||||||
|
|
||||||||||||||||||
| ``commands.run``, ``files.*`` and ``is_running`` take ``request_timeout`` | ||||||||||||||||||
| only -- the sandbox already carries the connection config, and handing | ||||||||||||||||||
| them the full ``ApiParams`` raises ``TypeError: unexpected keyword | ||||||||||||||||||
| argument 'api_key'``. | ||||||||||||||||||
| """ | ||||||||||||||||||
| """Return the request timeout; data-plane methods reject connection parameters.""" | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new one-liner no longer holds for every caller.
Suggested change
|
||||||||||||||||||
| if self._connection.request_timeout_s is None: | ||||||||||||||||||
| return {} | ||||||||||||||||||
| return {"request_timeout": self._connection.request_timeout_s} | ||||||||||||||||||
|
|
@@ -460,6 +459,11 @@ async def create(self, spec: SandboxSpec) -> SandboxHandle: | |||||||||||||||||
| "secure": self._create.secure, | ||||||||||||||||||
| **self._api_params(), | ||||||||||||||||||
| } | ||||||||||||||||||
| if self._create.auto_resume is not None: | ||||||||||||||||||
| kwargs["lifecycle"] = { | ||||||||||||||||||
| "on_timeout": "pause" if self._create.auto_resume else "kill", | ||||||||||||||||||
| "auto_resume": self._create.auto_resume, | ||||||||||||||||||
| } | ||||||||||||||||||
| if spec.ready_timeout_s is not None: | ||||||||||||||||||
| # E2B retains this on the returned sandbox connection. Subsequent | ||||||||||||||||||
| # provider calls explicitly reapply connection.request_timeout_s | ||||||||||||||||||
|
|
@@ -505,6 +509,10 @@ async def _create_with_extension_params(self, e2b: Any, kwargs: dict[str, Any]) | |||||||||||||||||
| } | ||||||||||||||||||
| if "network" in kwargs: | ||||||||||||||||||
| body["network"] = {_NETWORK_API_KEYS[key]: rules for key, rules in kwargs["network"].items()} | ||||||||||||||||||
| if "lifecycle" in kwargs: | ||||||||||||||||||
| # The SDK's wire form of the lifecycle, so both create paths time out alike. | ||||||||||||||||||
| body["autoPause"] = kwargs["lifecycle"]["auto_resume"] | ||||||||||||||||||
| body["autoResume"] = {"enabled": kwargs["lifecycle"]["auto_resume"]} | ||||||||||||||||||
| client = e2b.api.client_async.get_api_client(config).get_async_httpx_client() | ||||||||||||||||||
| response = await client.post("/sandboxes", json=body) | ||||||||||||||||||
| if response.status_code >= 300: | ||||||||||||||||||
|
|
@@ -608,12 +616,15 @@ async def status(self, handle: SandboxHandle) -> SandboxStatus: | |||||||||||||||||
| if isinstance(exc, type) | ||||||||||||||||||
| ) | ||||||||||||||||||
| try: | ||||||||||||||||||
| running = await sandbox.is_running(**self._request_params()) | ||||||||||||||||||
| # Control-plane status avoids waking paused sandboxes with a health check. | ||||||||||||||||||
| info = await sandbox.get_info(**self._request_params()) | ||||||||||||||||||
| except not_found: | ||||||||||||||||||
| return SandboxStatus.STOPPED | ||||||||||||||||||
| except Exception: # noqa: BLE001 - status must not raise for transient issues | ||||||||||||||||||
| return SandboxStatus.UNKNOWN | ||||||||||||||||||
| return SandboxStatus.RUNNING if running else SandboxStatus.STOPPED | ||||||||||||||||||
| return {"running": SandboxStatus.RUNNING, "paused": SandboxStatus.PAUSED}.get( | ||||||||||||||||||
| info.state, SandboxStatus.UNKNOWN | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| async def close(self, handle: SandboxHandle) -> None: | ||||||||||||||||||
| e2b = _require_e2b_sdk() | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this line runs to 116 characters while the rest of the page wraps at about 96. A reflow, which also drops the version from the old 300-second note (2.46 still uses 300):