Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions sdk-python/agentcube/clients/code_interpreter_data_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,22 @@ def execute_command(self, command: Union[str, List[str]], timeout: Optional[floa
resp.raise_for_status()

result = resp.json()
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")

if result["exit_code"] != 0:
raise CommandExecutionError(
exit_code=result["exit_code"],
stderr=result["stderr"],
stdout=stdout,
stderr=stderr,
command=command
)
Comment on lines 156 to 162
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The raise CommandExecutionError(...) line is over-indented relative to the surrounding if block, which is easy to miss in reviews and can trip formatters/lint rules. Align the indentation to a single level inside the if result["exit_code"] != 0: block.

Copilot uses AI. Check for mistakes.

return result["stdout"]
# Combine stdout and stderr for the caller if stderr is present
output = stdout
if stderr:
output = f"{stdout}\n{stderr}".strip() if stdout else stderr
return output

def run_code(self, language: str, code: str, timeout: Optional[float] = None) -> str:
"""Run a code snippet (python or bash)."""
Expand Down
6 changes: 3 additions & 3 deletions sdk-python/agentcube/code_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import os
import logging
from typing import Optional
from typing import Optional, Union

from agentcube.clients.control_plane import ControlPlaneClient
from agentcube.clients.code_interpreter_data_plane import CodeInterpreterDataPlaneClient
Expand Down Expand Up @@ -194,12 +194,12 @@ def run_code(self, language: str, code: str, timeout: Optional[float] = None) ->
"""
return self.dp_client.run_code(language, code, timeout)

def write_file(self, content: str, remote_path: str):
def write_file(self, content: Union[str, bytes], remote_path: str):
"""
Write content to a file in the remote environment.

Args:
content: The string content to write to the file.
content: The string or binary content to write to the file.
remote_path: The destination path of the file in the remote environment.
This path is relative to the session's working directory.
"""
Expand Down
10 changes: 8 additions & 2 deletions sdk-python/agentcube/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ class AgentCubeError(Exception):

class CommandExecutionError(AgentCubeError):
"""Raised when a command execution fails (exit code != 0)"""
def __init__(self, exit_code, stderr, command=None):
def __init__(self, exit_code: int, stdout: str, stderr: str, command: str = None):
self.exit_code = exit_code
Comment on lines +21 to 22
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

command is annotated as str = None, which is an invalid type/default pairing and will be flagged by type checkers. Use an optional type (e.g., command: str | None = None) and consider widening it to match callers (the data-plane client can pass a list of argv).

Copilot uses AI. Check for mistakes.
self.stdout = stdout
self.stderr = stderr
self.command = command
Comment on lines +21 to 25
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Type hint mismatch: command defaults to None but is annotated as str. This should be Optional[str] (or str | None) to match the actual allowed value and avoid type-checker errors.

Copilot uses AI. Check for mistakes.
super().__init__(f"Command failed (exit {exit_code}): {stderr}")

# Combine for the error message
output = stdout
if stderr:
output = f"{stdout}\n{stderr}".strip() if stdout else stderr
super().__init__(f"Command failed (exit {exit_code}): {output}")

class SessionError(AgentCubeError):
"""Raised when session creation or management fails"""
Expand Down
15 changes: 15 additions & 0 deletions sdk-python/agentcube/integrations/__init__.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please move the sandboxer provider in a separate dir. Utimately, i would like to see it is accepted in langchain similar as aws agent core.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""AgentCube SDK integrations."""
28 changes: 28 additions & 0 deletions sdk-python/agentcube/integrations/langchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# AgentCube LangChain Integration

This directory contains the AgentCube sandbox provider for LangChain and the DeepAgents ecosystem.

## Features

- **AgentCubeSandbox**: A sandbox provider for executing code in AgentCube sessions.
- **Async Support**: Fully non-blocking async methods using `asyncio.to_thread`.
- **Combined Output**: Merges stdout and stderr for better agent reasoning.

## Installation

```bash
pip install agentcube-sdk[langchain]
```

## Usage

```python
from agentcube import CodeInterpreterClient
from agentcube.integrations.langchain import AgentCubeSandbox

client = CodeInterpreterClient()
sandbox = AgentCubeSandbox(client)

response = sandbox.execute("print('hello world')")
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The usage example calls sandbox.execute("print('hello world')"), but execute runs a shell command in the sandbox. As written, this will fail on most systems unless there happens to be a print(...) executable. Use a shell command (e.g., python3 -c ... or echo ...) in the example to match the implementation.

Suggested change
response = sandbox.execute("print('hello world')")
response = sandbox.execute("python3 -c \"print('hello world')\"")

Copilot uses AI. Check for mistakes.
print(response.output)
```
17 changes: 17 additions & 0 deletions sdk-python/agentcube/integrations/langchain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .sandbox import AgentCubeSandbox

__all__ = ["AgentCubeSandbox"]
203 changes: 203 additions & 0 deletions sdk-python/agentcube/integrations/langchain/sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""LangChain integration for AgentCube Code Interpreter."""

from __future__ import annotations

import os
import tempfile
import asyncio
from typing import Optional

from agentcube.code_interpreter import CodeInterpreterClient
from agentcube.exceptions import CommandExecutionError

# Internal types for base class compliance
try:
from deepagents.backends.protocol import (
ExecuteResponse,
FileDownloadResponse,
FileUploadResponse,
)
from deepagents.backends.sandbox import BaseSandbox
except ModuleNotFoundError as e:
# Catching only if deepagents itself is missing.
# If deepagents is installed but has internal import errors, we should let them bubble up.
if e.name and (e.name.startswith("deepagents") or "deepagents" in e.name):
# Define fallback classes if deepagents is not installed
# This allows the module to be imported even if the optional integration
# dependencies are missing.
class BaseSandbox: # type: ignore
"""Fallback BaseSandbox."""
pass

class ExecuteResponse: # type: ignore
"""Fallback ExecuteResponse."""
def __init__(self, output: str, exit_code: int, truncated: bool = False):
self.output = output
self.exit_code = exit_code
self.truncated = truncated

class FileUploadResponse: # type: ignore
"""Fallback FileUploadResponse."""
def __init__(self, path: str, error: Optional[str] = None):
self.path = path
self.error = error

class FileDownloadResponse: # type: ignore
"""Fallback FileDownloadResponse."""
def __init__(self, path: str, content: bytes, error: Optional[str] = None):
self.path = path
self.content = content
self.error = error
else:
raise
except ImportError as e:
# Re-raise with more context if it's an import error within deepagents
raise ImportError(f"Failed to import deepagents: {e}") from e


class AgentCubeSandbox(BaseSandbox):
"""AgentCube implementation of the LangChain Sandbox integration.

This class allows AgentCube to be used as a backend for autonomous agents
and code execution tools within the LangChain / DeepAgents ecosystem.
"""

def __init__(self, client: CodeInterpreterClient) -> None:
"""Initialize the sandbox with an AgentCube CodeInterpreterClient.

Args:
client: An instance of AgentCube's CodeInterpreterClient.
"""
self._client = client

@property
def id(self) -> str:
"""Return the unique session ID of the sandbox instance."""
return self._client.session_id or "unknown"

def execute(
self,
command: str,
*,
timeout: int | None = None,
) -> ExecuteResponse:
"""Execute a shell command in the AgentCube sandbox.

Args:
command: The command to execute.
timeout: Optional execution timeout in seconds.

Returns:
An ExecuteResponse containing stdout, exit_code and truncated status.
"""
try:
# Map AgentCube output to ExecuteResponse
# execute_command now returns combined stdout and stderr
output = self._client.execute_command(command, timeout=timeout)
return ExecuteResponse(
output=output,
exit_code=0,
truncated=False,
)
except CommandExecutionError as e:
# Map AgentCube execution error
# Combine stdout and stderr for the agent
output = e.stdout
if e.stderr:
output = f"{output}\n{e.stderr}".strip() if output else e.stderr

return ExecuteResponse(
output=output,
exit_code=e.exit_code,
truncated=False,
)

def upload_files(
self,
files: list[tuple[str, bytes]],
) -> list[FileUploadResponse]:
"""Upload multiple files to the AgentCube sandbox.

Args:
files: A list of (remote_path, content_bytes) tuples.

Returns:
A list of FileUploadResponse objects in the same order as input.
"""
results = []
for path, content in files:
try:
# If bytes, try to decode to string as write_file currently only supports str
# SDK support for raw bytes will be added in a separate PR.
if isinstance(content, bytes):
content = content.decode("utf-8")
Comment on lines +144 to +147
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

upload_files decodes bytes to UTF-8 strings before calling write_file, but this PR updates CodeInterpreterClient.write_file to accept raw bytes and the LangChain file API is explicitly bytes. Decoding will corrupt binary data and can raise UnicodeDecodeError, and it also contradicts the method type hint (list[tuple[str, bytes]]). Pass bytes through unchanged (and only accept/convert str if needed).

Suggested change
# If bytes, try to decode to string as write_file currently only supports str
# SDK support for raw bytes will be added in a separate PR.
if isinstance(content, bytes):
content = content.decode("utf-8")

Copilot uses AI. Check for mistakes.
self._client.write_file(content, path)
Comment on lines +144 to +148
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

upload_files unconditionally decodes bytes as UTF-8 before calling write_file. This will corrupt arbitrary binary uploads and can raise UnicodeDecodeError for non-text content. Since the SDK is being updated to support bytes, prefer passing bytes through to self._client.write_file (and broaden the type to accept str | bytes if needed) rather than decoding here.

Copilot uses AI. Check for mistakes.
results.append(FileUploadResponse(path=path, error=None))
except Exception as e:
results.append(FileUploadResponse(path=path, error=str(e)))
return results

def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
"""Download multiple files from the AgentCube sandbox.

Args:
paths: A list of remote file paths to download.

Returns:
A list of FileDownloadResponse objects containing file contents.
"""
results = []
for path in paths:
try:
# AgentCube download_file writes to a local path
# We use a temp file to read it into memory for the response
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp_path = tmp.name

try:
self._client.download_file(path, tmp_path)
with open(tmp_path, "rb") as f:
content = f.read()
results.append(FileDownloadResponse(path=path, content=content, error=None))
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception as e:
results.append(FileDownloadResponse(path=path, content=b"", error=str(e)))
return results

# --- Async Support ---

async def aexecute(
self,
command: str,
*,
timeout: int | None = None,
) -> ExecuteResponse:
"""Async version of execute. Offloaded to thread pool."""
return await asyncio.to_thread(self.execute, command, timeout=timeout)

async def aupload_files(
self,
files: list[tuple[str, bytes]],
) -> list[FileUploadResponse]:
"""Async version of upload_files. Offloaded to thread pool."""
return await asyncio.to_thread(self.upload_files, files)

async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
"""Async version of download_files. Offloaded to thread pool."""
return await asyncio.to_thread(self.download_files, paths)
Loading
Loading