Repository navigation
feat(sdk): add throwOnError / raise_on_exception to Sandbox.fork - #2003
devin-ai-integration[bot] wants to merge 1 commit into
Conversation
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
🦋 Changeset detectedLatest commit: a261290 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Package ArtifactsBuilt from 635f44f. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-dockerfile-utils-0.1.2-devin-1792507515-fork-throw-on-error.0.tgz ./e2b-2.57.1-devin-1792507515-fork-throw-on-error.0.tgzCLI ( npm install ./e2b-cli-2.22.2-devin-1792507515-fork-throw-on-error.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.9.1-devin-1792507515-fork-throw-on-error.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.5.1-devin-1792507515-fork-throw-on-error.0.tgzPython SDK ( pip install ./e2b_dockerfile_utils-0.1.1+devin.1792507515.fork.throw.on.error-py3-none-any.whl ./e2b-2.57.0+devin.1792507515.fork.throw.on.error-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.11.1+devin.1792507515.fork.throw.on.error-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.7.0+devin.1792507515.fork.throw.on.error-py3-none-any.whl |
There was a problem hiding this comment.
TASTE.md review
Checked the changed code against the parity (T-1, T-2, T-10), API shape (T-3/T-3a, T-14, T-23, T-24, T-25), batch semantics (T-33), module structure (T-54), errors (T-57, T-62) and docstring (T-69 to T-72) rules.
3 violations, 6 inline comments:
- T-25: the Python
forkoverloads (sync + async, instance + static) have nobooloverload, so a non-literalraise_on_exceptiondoesn't type-check. Inline on the instance overloads; the static overloads atsandbox_sync/main.py:525andsandbox_async/main.py:530need the same fix. - T-25 / T-72: in JS, the
throwOnError: trueoverloads kept the old@returns("a sandbox instance or an error"), which is wrong for that overload. - T-62: the
SandboxForkError/SandboxForkExceptionmessage doesn't tell the user the successful forks are still running.
Not tied to a single line:
- Same per-variant doc drift in Python (T-25): the
Literal[True]overloads repeat the "either a running sandbox or an exception" paragraph, and theLiteral[False]overloads carry:raises SandboxForkException:even though that overload can't raise. Each overload's docstring should describe only its own variant. - Borderline T-1a / T-10:
throwOnError↔raise_on_exceptiondiffer by more than casing. T-1c only allows theError/Exceptionswap for class suffixes, and the strict mirror would bethrow_on_error.raise_on_exceptionreads as idiomatic Python, so this is a call for the maintainers, not a blocker.
Looks right: keyword-only * on the new Python param (T-3a); SandboxForkError extends SandboxError / SandboxForkException(SandboxException) (T-57, T-1c); exported from index.ts and __all__ (T-54); JS overloads narrow the return and keep a non-literal fallback (T-24/T-25); per-item outcomes are still available through .results (T-33); parity across JS, sync and async Python (T-1/T-2).
| @@ -412,17 +417,60 @@ export class Sandbox extends SandboxApi { | |||
| * @param opts fork options — `count`, `timeoutMs` and connection options. | |||
| * | |||
| * @returns array with one entry per requested fork — a sandbox instance or an error. | |||
There was a problem hiding this comment.
T-25 / T-72 — each overload carries its own docs, and @returns must say what this variant returns. This doc block now belongs to the throwOnError: true overload, which resolves to Sandbox[] only, but it still says "a sandbox instance or an error" (and the paragraph above still describes the allSettled-style mixed array). Describe the narrowed return here:
| * @returns array with one entry per requested fork — a sandbox instance or an error. | |
| * @returns array of running sandbox instances, one per requested fork, in request order. |
| @@ -492,17 +549,51 @@ export class Sandbox extends SandboxApi { | |||
| * @param opts fork options — `count`, `timeoutMs` and connection options. | |||
| * | |||
| * @returns array with one entry per requested fork — a sandbox instance or an error. | |||
There was a problem hiding this comment.
T-25 / T-72 — each overload carries its own docs, and @returns must say what this variant returns. This doc block now belongs to the throwOnError: true overload, which resolves to Sandbox[] only, but it still says "a sandbox instance or an error" (and the paragraph above still describes the allSettled-style mixed array). Describe the narrowed return here:
| * @returns array with one entry per requested fork — a sandbox instance or an error. | |
| * @returns array of running sandbox instances, one per requested fork, in request order. |
| timeout: Optional[int] = None, | ||
| count: Optional[int] = None, | ||
| *, | ||
| raise_on_exception: Literal[True], |
There was a problem hiding this comment.
T-25 — a boolean-discriminated pair needs a third overload for the non-literal case. Only Literal[False] and Literal[True] overloads exist, so a real call site such as sandbox.fork(count=2, raise_on_exception=strict) with strict: bool matches neither and pyright/mypy report "no overload matches". (JS gets this right: its opts?: SandboxForkOpts overload takes throwOnError?: boolean.)
Add a bool overload returning the union, with its own docstring, here and on the static fork overload (sandbox_id: str, … at line 525):
@overload
def fork(
self,
timeout: Optional[int] = None,
count: Optional[int] = None,
*,
raise_on_exception: bool,
**opts: Unpack[ApiParams],
) -> List[Union[Self, Exception]]:
"""<own docstring: may raise SandboxForkException when raise_on_exception is True>"""
...| timeout: Optional[int] = None, | ||
| count: Optional[int] = None, | ||
| *, | ||
| raise_on_exception: Literal[True], |
There was a problem hiding this comment.
T-25 — a boolean-discriminated pair needs a third overload for the non-literal case. Only Literal[False] and Literal[True] overloads exist, so a real call site such as sandbox.fork(count=2, raise_on_exception=strict) with strict: bool matches neither and pyright/mypy report "no overload matches". (JS gets this right: its opts?: SandboxForkOpts overload takes throwOnError?: boolean.)
Add a bool overload returning the union, with its own docstring, here and on the static fork overload (sandbox_id: str, … at line 530):
@overload
async def fork(
self,
timeout: Optional[int] = None,
count: Optional[int] = None,
*,
raise_on_exception: bool,
**opts: Unpack[ApiParams],
) -> List[Union[Self, Exception]]:
"""<own docstring: may raise SandboxForkException when raise_on_exception is True>"""
...| `${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors | ||
| .map((error) => error.message) | ||
| .join('; ')}` |
There was a problem hiding this comment.
T-62 — error messages are actionable: say what to do, not just what failed. The key thing a user must act on is that the forks that did start keep running (and billing) after this is raised; that is only in the class docstring, not in the message that ends up in logs. Put it in the message:
| `${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors | |
| .map((error) => error.message) | |
| .join('; ')}` | |
| `${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors | |
| .map((error) => error.message) | |
| .join('; ')}. The forks that did start are still running — use \`error.results\` to keep or kill them.` |
| f"{len(failed_errors)} of {len(results)} forks failed to start: " | ||
| f"{'; '.join(str(error) for error in failed_errors)}" |
There was a problem hiding this comment.
T-62 — error messages are actionable: say what to do, not just what failed. The key thing a user must act on is that the forks that did start keep running (and billing) after this is raised; that is only in the class docstring, not in the message that ends up in logs. Put it in the message:
| f"{len(failed_errors)} of {len(results)} forks failed to start: " | |
| f"{'; '.join(str(error) for error in failed_errors)}" | |
| f"{len(failed_errors)} of {len(results)} forks failed to start: " | |
| f"{'; '.join(str(error) for error in failed_errors)}. " | |
| "The forks that did start are still running — use `.results` to keep or kill them." |
Summary
Sandbox.forkreturns one entry per requested fork,Sandbox | Error(allSettled-style). This adds an opt-in flag that throws instead when any fork failed, carrying all results so the caller can still use or kill the forks that did start.SandboxForkOpts.throwOnError?: boolean→ throwsSandboxForkError extends SandboxErrorwithresults: Array<Sandbox | Error>. Overloads narrow the return type toSandbox[](this[]/InstanceType<S>[]) whenthrowOnError: true.raise_on_exception: bool = False→ raisesSandboxForkException(SandboxException)with.results.Literal[True]overloads returnList[Self]/List["Sandbox"]/List["AsyncSandbox"]."1 of 2 forks failed to start: <per-fork messages joined by '; '>".Tests are offline and mock the fork response (one success plus one 429):
packages/js-sdk/tests/sandbox/forkThrowOnError.test.tsandpackages/python-sdk/tests/shared/sandbox/test_fork_raise_on_exception.py.Link to Devin session: https://app.devin.ai/sessions/b5575b1ac20941429b771ed858d33ef6
Open in Devin Desktop: https://app.devin.ai/desktop/session/b5575b1ac20941429b771ed858d33ef6?variant=devin
Requested by: @mishushakov