Skip to content

feat(sdk): add streamInput / stream_input for commands and pty - #1997

Merged
mishushakov merged 7 commits into
mainfrom
devin/1792417976-stream-input
Oct 20, 2026
Merged

mishushakov merged 7 commits into
mainfrom
devin/1792417976-stream-input

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Exposes envd's client-streaming process.Process/StreamInput RPC: one long-lived ordered input stream per process instead of one SendInput request per chunk (sequential pty.sendInput() was the latency bottleneck in SDK-393).

const input = sandbox.pty.streamInput(pid, { timeoutMs: 0 }) // also sandbox.commands.streamInput → CommandInputHandle
await input.send('ls -la\n')        // string | Uint8Array, enqueued in order
await input.close()                 // ends the stream; throws the mapped RPC error (e.g. NotFoundError)
# async: background-pumped handle
async with sandbox.commands.stream_input(pid, timeout=0) as handle:   # AsyncCommandInputHandle
    await handle.send("data")

# sync: no background threads (TASTE T-28) — blocks while it consumes the iterable
sandbox.pty.stream_input(pid, (chunk for chunk in chunks))
  • JS / async Python: send enqueues and returns immediately; the RPC drains the queue as start → data… and sends a keepalive after KEEPALIVE_PING_INTERVAL_SEC (50s) idle. Sync Python sends each item as the iterable yields it, without keepalives.
  • Timeout default STREAM_INPUT_TIMEOUT_MS / STREAM_INPUT_TIMEOUT (60s), 0 = no limit; Python options are keyword-only. Closing the input stream does not close the process' stdin (use closeStdin).
  • JS transport: connect-web refuses client streaming (The fetch API does not support streaming request bodies), so src/envd/streamingTransport.ts builds a Connect transport (createTransport from @connectrpc/connect/protocol-connect) with a small httpClient that sends a ReadableStream body with duplex: 'half' through the same envd RPC fetch (headers, access token, undici dispatcher). All other RPCs stay on connect-web. Browsers need fetch upload streaming (Chromium over HTTP/2).
  • Python: DefaultHeadersInterceptor/LoggingInterceptor gained intercept_client_stream(_sync); without them sandbox headers were missing and the proxy answered Invalid host.
  • Verified live: chunks reach the process as they are sent over HTTP/2 and HTTP/1.1 (JS + Python).

Link to Devin session: https://app.devin.ai/sessions/40d699b8846f4ceabbdbcfdfec2b2012
Open in Devin Desktop: https://app.devin.ai/desktop/session/40d699b8846f4ceabbdbcfdfec2b2012?variant=devin
Requested by: @mishushakov

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@cla-bot cla-bot Bot added the cla-signed label Oct 19, 2026
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5247ed5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
e2b Minor
@e2b/python-sdk Minor
@e2b/code-interpreter-python Patch

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

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 2bf12d8. Download artifacts from this workflow run.

JS SDK (e2b@2.57.1-devin-1792417976-stream-input.0, with @e2b/dockerfile-utils@0.1.2-devin-1792417976-stream-input.0):

npm install ./e2b-dockerfile-utils-0.1.2-devin-1792417976-stream-input.0.tgz ./e2b-2.57.1-devin-1792417976-stream-input.0.tgz

CLI (@e2b/cli@2.22.3-devin-1792417976-stream-input.0):

npm install ./e2b-cli-2.22.3-devin-1792417976-stream-input.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.9.1-devin-1792417976-stream-input.0):

npm install ./e2b-code-interpreter-2.9.1-devin-1792417976-stream-input.0.tgz

Desktop JS SDK (@e2b/desktop@2.5.1-devin-1792417976-stream-input.0):

npm install ./e2b-desktop-2.5.1-devin-1792417976-stream-input.0.tgz

Python SDK (e2b==2.57.0+devin.1792417976.stream.input, with e2b-dockerfile-utils==0.1.1+devin.1792417976.stream.input):

pip install ./e2b_dockerfile_utils-0.1.1+devin.1792417976.stream.input-py3-none-any.whl ./e2b-2.57.0+devin.1792417976.stream.input-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.11.1+devin.1792417976.stream.input):

pip install ./e2b_code_interpreter-2.11.1+devin.1792417976.stream.input-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.7.0+devin.1792417976.stream.input):

pip install ./e2b_desktop-2.7.0+devin.1792417976.stream.input-py3-none-any.whl

devin-ai-integration[bot]

This comment was marked as resolved.

@linear-code

linear-code Bot commented Oct 20, 2026

Copy link
Copy Markdown

SDK-393

@mishushakov

Copy link
Copy Markdown
Member

align with TASTE pls

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Aligned with TASTE in 691f8aa:

  • T-26: renamed to CommandInputHandle (JS) and AsyncCommandInputHandle (async Python).
  • T-28: sync Python no longer starts a thread. stream_input(pid, data: Iterable[str | bytes], *, timeout=...) blocks on the calling thread and sends each item as the iterable yields it, without keepalives.
  • T-3a: Python options are keyword-only.
  • T-47: the timeout defaults are named constants, STREAM_INPUT_TIMEOUT_MS / STREAM_INPUT_TIMEOUT.
  • T-63: the error for sending after close now says how to open a new handle.
  • T-69–71: added full JSDoc and reST docs.

The PR description is updated to match.

@mishushakov
mishushakov marked this pull request as ready for review October 20, 2026 14:46
@mishushakov
mishushakov self-requested a review as a code owner October 20, 2026 14:46
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T14:49:45.040295Z 7b5b358 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

devin-ai-integration[bot]

This comment was marked as resolved.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 packages/js-sdk/src/sandbox/commands/inputHandle.ts — JS callers who reuse a Uint8Array buffer after await input.send(buf) can send corrupted or duplicated bytes, because the handle sends the buffer later. send() pushes the caller's array by reference and resolves at once. requests() only reads it when the RPC pulls the next message (inputHandle.ts:395). Fix: when send() gets a Uint8Array, queue a copy (data.slice()), so later changes by the caller cannot reach the wire; leave strings as they are.

    Why this was flagged

    A caller reads a file, socket or pty source into one reused buffer, e.g. fs.read(fd, buf, ...) in a loop, and calls await handle.send(buf.subarray(0, n)) for each chunk. That is the pattern this API invites, since send resolves once the input is queued (inputHandle.ts:310). send() at inputHandle.ts:325 queues the caller's own Uint8Array. requests() encodes it only when the connect transport pulls the next request (inputHandle.ts:388-409), which is after the caller has already overwritten the buffer for the next chunk. The PTY or stdin then gets the later chunk's bytes, possibly twice, and the earlier chunk is lost. With the existing sendInput/sendStdin, the caller awaits a full request that serializes the bytes before it returns. Python is not affected because bytes cannot be changed.

    Verification: nit. The push is at line 106: this.queue.push(data) stores the caller's array by reference, and send resolves right after notify() (lines 106-107). The array is read only later, in requests() (lines 171-177). The existing sendInput waits for the RPC, so buffer reuse after its await was safe.

  • 🟡 packages/python-sdk/e2b/sandbox_sync/commands/pty.py — Sync users who pass a single bytes or str to stream_input(pid, data) get a confusing failure or one-character messages, not one input chunk. requests() iterates data without checking its type. Iterating bytes yields ints, so stream_input_data gets an int payload and serialization fails. Iterating a str sends one message per character. Fix: in both sync stream_input methods, treat a bare str/bytes/bytearray as one chunk (or raise InvalidArgumentException) before iterating. Same pattern at 2 sites (sandbox_sync/commands/pty.py:1116, sandbox_sync/commands/command.py:1048).

    Why this was flagged

    The trigger is a call like sandbox.pty.stream_input(pid, b"ls\n") or sandbox.commands.stream_input(pid, "hello\n"). That is the natural sync counterpart of JS input.send(data) and async handle.send(data), both of which take one str | bytes. In requests() (sandbox_sync/commands/pty.py and sandbox_sync/commands/command.py), for item in data walks a bytes object as ints. stream_input_data (e2b/sandbox/commands/stream_input.py) then passes the int straight through as payload, because only str gets encoded. The error is raised inside the request generator that connectrpc consumes. It then goes through handle_rpc_exception_with_health and comes out as a generic RPC failure rather than a clear argument error. A str argument silently becomes one StreamInput data message per character. No type guard exists. The annotation Iterable[Union[str, bytes]] is not enforced at runtime, and a str satisfies it anyway. This is a new API, so the base branch has no equivalent call.

    Verification: In both sync methods (packages/python-sdk/e2b/sandbox_sync/commands/pty.py and sandbox_sync/commands/command.py), requests() does for item in data: yield stream_input_data("pty"|"stdin", item) without checking the type. So iterating a bytes value hands it an int, which goes into Oneof("pty", payload) and fails during serialization.

@mishushakov

Copy link
Copy Markdown
Member

check the findings above

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Checked the findings: all three are valid, and 49ea9df fixes them.

  • Async Python close() cancellation (Devin Review): close() now shields the RPC task.
  • JS Uint8Array reuse (Devin Review + Claude): send() now queues a copy of the array.
  • Sync Python with a single str/bytes (Claude): passing b"ls\n" used to iterate it as ints and fail as an unclear RPC error, and a str was sent one character per message. stream_input now raises InvalidArgumentException before any request, and the message says to wrap the value, e.g. stream_input(pid, [data]).

Each fix has a regression test. The targeted JS and Python tests, format, lint and typecheck pass.

devin-ai-integration Bot and others added 7 commits October 20, 2026 19:15
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
@mishushakov
mishushakov force-pushed the devin/1792417976-stream-input branch from 49ea9df to 5247ed5 Compare October 20, 2026 17:15
@mishushakov
mishushakov merged commit 3c4c7cf into main Oct 20, 2026
44 of 48 checks passed
@mishushakov
mishushakov deleted the devin/1792417976-stream-input branch October 20, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant