Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f98c652
added isolated code executor
AlexFierro9 Oct 20, 2025
b3cdd32
Update src/google/adk/code_executors/isolated_code_executor.py
AlexFierro9 Oct 20, 2025
a77b0c2
Update src/google/adk/code_executors/isolated_code_executor.py
AlexFierro9 Oct 20, 2025
8fbd73b
added the units tests
AlexFierro9 Oct 20, 2025
9cf3ef0
Merge branch 'main' into main
AlexFierro9 Oct 21, 2025
98e5a83
Merge branch 'main' into main
AlexFierro9 Oct 22, 2025
ff06b44
Merge branch 'main' into main
hangfei Oct 23, 2025
ba5dbbd
Merge branch 'main' into main
AlexFierro9 Oct 24, 2025
b0f3f6c
run autoformat.sh
AlexFierro9 Oct 24, 2025
1e49154
Merge branch 'main' of https://github.com/AlexFierro9/adk-python
AlexFierro9 Oct 24, 2025
5d01c91
Merge branch 'main' into main
AlexFierro9 Oct 25, 2025
6424fc4
Merge remote-tracking branch 'upstream/main'
AlexFierro9 Oct 30, 2025
c1e3487
Merge branch 'main' into main
hangfei Nov 5, 2025
039b9b9
Added Isolated code executor as a config option for unsafe local code…
AlexFierro9 Nov 9, 2025
0dc3c26
Merge branch 'main' of https://github.com/AlexFierro9/adk-python
AlexFierro9 Nov 9, 2025
6a9ee6b
Added Isolated Code Executor as a separate config
AlexFierro9 Nov 9, 2025
1cc3a2e
Merge branch 'main' into main
AlexFierro9 Nov 9, 2025
7603e57
got rid of isolated code executor file, added tests cases (with assis…
AlexFierro9 Nov 9, 2025
bf19756
Merge branch 'main' of https://github.com/AlexFierro9/adk-python
AlexFierro9 Nov 9, 2025
8f12cc6
Merge branch 'main' into main
AlexFierro9 Nov 9, 2025
453e786
changed use_isolated_process to use_separate_process, ran isort for f…
AlexFierro9 Nov 11, 2025
f742aea
Merge branch 'main' into main
AlexFierro9 Nov 11, 2025
ec4d631
Merge branch 'main' into main
hangfei Nov 11, 2025
1c07fa0
Revert "Added Isolated Code Executor as a separate config"
AlexFierro9 Nov 12, 2025
f64a8ab
Merge branch 'main' of https://github.com/AlexFierro9/adk-python
AlexFierro9 Nov 12, 2025
5840c02
Merge branch 'main' into main
AlexFierro9 Nov 12, 2025
55ffc93
Merge branch 'main' into main
hangfei Nov 14, 2025
ba6a56c
Merge branch 'main' into main
hangfei Nov 14, 2025
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
1 change: 0 additions & 1 deletion contributing/samples/gepa/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from tau_bench.types import EnvRunResult
from tau_bench.types import RunConfig
import tau_bench_agent as tau_bench_agent_lib

import utils


Expand Down
1 change: 0 additions & 1 deletion contributing/samples/gepa/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from absl import flags
import experiment
from google.genai import types

import utils

_OUTPUT_DIR = flags.DEFINE_string(
Expand Down
1 change: 1 addition & 0 deletions src/google/adk/code_executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from .base_code_executor import BaseCodeExecutor
from .built_in_code_executor import BuiltInCodeExecutor
from .isolated_code_executor import IsolatedCodeExecutor
from .code_executor_context import CodeExecutorContext
from .unsafe_local_code_executor import UnsafeLocalCodeExecutor

Expand Down
46 changes: 34 additions & 12 deletions src/google/adk/code_executors/unsafe_local_code_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from contextlib import redirect_stdout
import io
import logging
import re
import subprocess
import sys
from typing import Any

from pydantic import Field
Expand All @@ -38,7 +39,10 @@ def _prepare_globals(code: str, globals_: dict[str, Any]) -> None:


class UnsafeLocalCodeExecutor(BaseCodeExecutor):
"""A code executor that unsafely execute code in the current local context."""
"""A code executor that unsafely execute code in the current local context.

This executor can be configured to run code in an isolated process.
"""

# Overrides the BaseCodeExecutor attribute: this executor cannot be stateful.
stateful: bool = Field(default=False, frozen=True, exclude=True)
Expand All @@ -47,39 +51,57 @@ class UnsafeLocalCodeExecutor(BaseCodeExecutor):
# optimize_data_file.
optimize_data_file: bool = Field(default=False, frozen=True, exclude=True)

def __init__(self, **data):
use_separate_process: bool = False

def __init__(self, use_separate_process: bool = False, **data):
"""Initializes the UnsafeLocalCodeExecutor."""
if 'stateful' in data and data['stateful']:
raise ValueError('Cannot set `stateful=True` in UnsafeLocalCodeExecutor.')
if 'optimize_data_file' in data and data['optimize_data_file']:
raise ValueError(
'Cannot set `optimize_data_file=True` in UnsafeLocalCodeExecutor.'
)
super().__init__(**data)
super().__init__(use_separate_process=use_separate_process, **data)
self.use_separate_process = use_separate_process

@override
def execute_code(
self,
invocation_context: InvocationContext,
code_execution_input: CodeExecutionInput,
) -> CodeExecutionResult:
if self.use_separate_process:
logger.debug(
'Executing code in isolated process:\n```\n%s\n```',
code_execution_input.code,
)
process_result = subprocess.run(
[sys.executable, '-c', code_execution_input.code],
capture_output=True,
text=True,
)
return CodeExecutionResult(
stdout=process_result.stdout,
stderr=process_result.stderr,
output_files=[],
)

logger.debug('Executing code:\n```\n%s\n```', code_execution_input.code)
# Execute the code.
output = ''
error = ''
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
try:
globals_ = {}
_prepare_globals(code_execution_input.code, globals_)
stdout = io.StringIO()
with redirect_stdout(stdout):
with redirect_stdout(stdout_capture):
exec(code_execution_input.code, globals_)
output = stdout.getvalue()
except Exception as e:
error = str(e)
import traceback
stderr_capture.write(traceback.format_exc())

# Collect the final result.
return CodeExecutionResult(
stdout=output,
stderr=error,
stdout=stdout_capture.getvalue(),
stderr=stderr_capture.getvalue(),
output_files=[],
)
112 changes: 112 additions & 0 deletions tests/unittests/code_executors/test_isolated_code_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import os
from unittest.mock import MagicMock

from google.adk.agents.base_agent import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
from google.adk.code_executors.code_execution_utils import CodeExecutionResult
from google.adk.code_executors.isolated_code_executor import IsolatedCodeExecutor
from google.adk.sessions.base_session_service import BaseSessionService
from google.adk.sessions.session import Session
import pytest


@pytest.fixture
def mock_invocation_context() -> InvocationContext:
"""Provides a mock InvocationContext."""
mock_agent = MagicMock(spec=BaseAgent)
mock_session = MagicMock(spec=Session)
mock_session_service = MagicMock(spec=BaseSessionService)
return InvocationContext(
invocation_id="test_invocation",
agent=mock_agent,
session=mock_session,
session_service=mock_session_service,
)


class TestIsolatedCodeExecutor:

def test_init_default(self):
executor = IsolatedCodeExecutor()
assert not executor.stateful
assert not executor.optimize_data_file

def test_init_stateful_raises_error(self):
with pytest.raises(
ValueError,
match="Cannot set `stateful=True` in IsolatedCodeExecutor.",
):
IsolatedCodeExecutor(stateful=True)

def test_init_optimize_data_file_raises_error(self):
with pytest.raises(
ValueError,
match=(
"Cannot set `optimize_data_file=True` in IsolatedCodeExecutor."
),
):
IsolatedCodeExecutor(optimize_data_file=True)

def test_execute_code_simple_print(
self, mock_invocation_context: InvocationContext
):
executor = IsolatedCodeExecutor()
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)

assert isinstance(result, CodeExecutionResult)
assert result.stdout == "hello world\n"
assert result.stderr == ""
assert result.output_files == []

def test_execute_code_with_error(
self, mock_invocation_context: InvocationContext
):
executor = IsolatedCodeExecutor()
code_input = CodeExecutionInput(code='raise ValueError("Test error")')
result = executor.execute_code(mock_invocation_context, code_input)

assert isinstance(result, CodeExecutionResult)
assert result.stdout == ""
assert "Test error" in result.stderr

Choose a reason for hiding this comment

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

medium

The test asserts that "Test error" is in result.stderr. It would be more robust to assert that the ValueError exception type is also present in the stderr. This ensures that the correct exception is being raised and captured.

Suggested change
assert "Test error" in result.stderr
assert "ValueError" in result.stderr
assert "Test error" in result.stderr

assert result.output_files == []

def test_execute_code_variable_assignment(
self, mock_invocation_context: InvocationContext
):
executor = IsolatedCodeExecutor()
code_input = CodeExecutionInput(code="x = 10\nprint(x * 2)")
result = executor.execute_code(mock_invocation_context, code_input)

assert result.stdout == "20\n"
assert result.stderr == ""

def test_execute_code_empty(self, mock_invocation_context: InvocationContext):
executor = IsolatedCodeExecutor()
code_input = CodeExecutionInput(code="")
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert result.stderr == ""

def test_execute_code_with_import(
self, mock_invocation_context: InvocationContext
):
executor = IsolatedCodeExecutor()
code = "import os; print(os.linesep)"
code_input = CodeExecutionInput(code=code)
result = executor.execute_code(mock_invocation_context, code_input)

assert result.stdout.strip() == os.linesep.strip()
assert result.stderr == ""

def test_execute_code_multiline_output(
self, mock_invocation_context: InvocationContext
):
executor = IsolatedCodeExecutor()
code = 'print("line 1")\nprint("line 2")'
code_input = CodeExecutionInput(code=code)
result = executor.execute_code(mock_invocation_context, code_input)

assert result.stdout == "line 1\nline 2\n"
assert result.stderr == ""
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# 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.

import os
from unittest.mock import MagicMock

from google.adk.agents.base_agent import BaseAgent
Expand Down Expand Up @@ -101,3 +101,83 @@ def test_execute_code_empty(self, mock_invocation_context: InvocationContext):
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert result.stderr == ""

def test_execute_code_isolated_simple_print(
self, mock_invocation_context: InvocationContext
):
"""Test execution with use_separate_process=True."""
executor = UnsafeLocalCodeExecutor(use_separate_process=True)
code_input = CodeExecutionInput(code='print("hello isolated world")')
result = executor.execute_code(mock_invocation_context, code_input)

assert isinstance(result, CodeExecutionResult)
assert result.stdout == "hello isolated world\n"
assert result.stderr == ""
assert result.output_files == []

def test_execute_code_isolated_with_error(
self, mock_invocation_context: InvocationContext
):
"""Test error handling with use_separate_process=True."""
executor = UnsafeLocalCodeExecutor(use_separate_process=True)
code_input = CodeExecutionInput(code='raise ValueError("Isolated error")')
result = executor.execute_code(mock_invocation_context, code_input)

assert isinstance(result, CodeExecutionResult)
assert result.stdout == ""
assert "Isolated error" in result.stderr
assert result.output_files == []

def test_execute_code_isolated_with_import(
self, mock_invocation_context: InvocationContext
):
"""Test imports with use_separate_process=True."""
executor = UnsafeLocalCodeExecutor(use_separate_process=True)
code = "import os; print(os.linesep)"
code_input = CodeExecutionInput(code=code)
result = executor.execute_code(mock_invocation_context, code_input)

assert result.stdout.strip() == os.linesep.strip()
assert result.stderr == ""

def test_execute_code_if_main(
self, mock_invocation_context: InvocationContext
):
"""Test code with `if __name__ == '__main__'`."""
executor = UnsafeLocalCodeExecutor()
code = 'if __name__ == "__main__":\n print("executed as main")'
code_input = CodeExecutionInput(code=code)
result = executor.execute_code(mock_invocation_context, code_input)

assert result.stdout == "executed as main\n"
assert result.stderr == ""

def test_execute_code_isolated_memory_isolation_variable(
self, mock_invocation_context: InvocationContext
):
"""Test that variables do not persist between isolated executions."""
executor = UnsafeLocalCodeExecutor(use_separate_process=True)

# First execution defines a variable.
code_input1 = CodeExecutionInput(code="x = 100")
executor.execute_code(mock_invocation_context, code_input1)

# Second execution tries to access the variable.
code_input2 = CodeExecutionInput(code="print(x)")
result = executor.execute_code(mock_invocation_context, code_input2)

assert "name 'x' is not defined" in result.stderr

def test_execute_code_isolated_memory_isolation_function(
self, mock_invocation_context: InvocationContext
):
"""Test that functions do not persist between isolated executions."""
executor = UnsafeLocalCodeExecutor(use_separate_process=True)

code_input1 = CodeExecutionInput(code="def my_func(): return 'isolated'")
executor.execute_code(mock_invocation_context, code_input1)

code_input2 = CodeExecutionInput(code="print(my_func())")
result = executor.execute_code(mock_invocation_context, code_input2)

assert "name 'my_func' is not defined" in result.stderr
Loading