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
46 changes: 37 additions & 9 deletions docs/guides/code_executors/code_executor/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ doing so as you.
`BaseCodeExecutor` puts that whole decision behind one object. The agent hands it
a code block and gets back standard output, standard error, and any files the
code produced, while everything about where the code actually ran stays the
implementation's business. Six implementations ship in
`google.adk.code_executors`, and a seventh lives alongside its integration in
implementation's business. Seven implementations ship in
`google.adk.code_executors`, and an eighth lives alongside its integration in
`google.adk.integrations.cloud_run`. What separates them is almost entirely how
much isolation they give you.

Expand Down Expand Up @@ -95,7 +95,7 @@ implementation:
`optimize_data_file` makes the flow search the user's message for `text/csv`
parts, parse them, and put them on `CodeExecutionInput.input_files`. The
generated code can then load the dataset by filename, with no upload code of your
own anywhere. Only three implementations accept it, though.
own anywhere. Only four implementations accept it, though.

`stateful` says whether a variable defined in one turn is still around in the
next. A stateless executor starts a fresh process every time, so the model has to
Expand Down Expand Up @@ -148,6 +148,10 @@ the model writes.

* **`ContainerCodeExecutor`** starts a local or self-hosted Docker container
with the network disabled and Linux capabilities dropped.
* **`SmolCodeExecutor`** creates and deletes a local microVM for each snippet,
with guest networking disabled by default. It needs Linux KVM or macOS
Hypervisor.framework, but no container daemon. Set `target="cloud"` to
run the same executor on Smol Cloud instead.
* **`GkeCodeExecutor`** runs the snippet on a Kubernetes cluster in
gVisor-sandboxed Pods, or through the Agent Sandbox client, so the code
talks to a sandboxed kernel rather than the node's own.
Expand Down Expand Up @@ -175,10 +179,10 @@ account, a quota, and code executing somewhere you do not administer.
Once you have settled on a level of trust, two details can take a choice away
from you again.

If your agent needs a variable to survive from one snippet to the next, or wants
a CSV attached for it, three of the seven are already out.
`UnsafeLocalCodeExecutor`, `ContainerCodeExecutor` and
`CloudRunSandboxCodeExecutor` reject `stateful` and `optimize_data_file`.
If your agent needs a variable to survive from one snippet to the next,
`SmolCodeExecutor` is also stateless: it creates a fresh VM per code block.
For attached CSV files, `UnsafeLocalCodeExecutor`, `ContainerCodeExecutor`
and `CloudRunSandboxCodeExecutor` reject `optimize_data_file`.

If you pick `CloudRunSandboxCodeExecutor`, read its options before you configure
it, because it is the one implementation that changes a base default. Its
Expand Down Expand Up @@ -216,6 +220,29 @@ agent = LlmAgent(
)
```

### Run in a local or Cloud microVM

Install `pip install "google-adk[smol]"` and use a local VM without a Docker daemon:

```python
from google.adk.agents import LlmAgent
from google.adk.code_executors import SmolCodeExecutor

agent = LlmAgent(
name="microvm_code_agent",
code_executor=SmolCodeExecutor(timeout_seconds=60),
)
```

The executor creates a fresh VM per code block, uploads Python and any input
files into `/workspace`, runs the code with a bounded timeout, and deletes
the VM. Set `optimize_data_file=True` to attach CSV files from the request. Guest egress is off by default. To run on Smol Cloud, provide
`SMOL_CLOUD_TOKEN` and set `target="cloud"`; `network_enabled=True` explicitly
allows the guest to access the network. Cloud VMs also have an expiry time in
case the client process stops before deletion. Each code block starts with a
clean filesystem, so Python variables and generated files do not persist
between blocks. The SDK supports Linux and macOS hosts.

### Run in a gVisor sandbox on Kubernetes

`GkeCodeExecutor` creates one short-lived Job per execution on the gVisor
Expand Down Expand Up @@ -252,11 +279,12 @@ agent = LlmAgent(
`UnsafeLocalCodeExecutor`, `ContainerCodeExecutor` and
`CloudRunSandboxCodeExecutor` raise `ValueError` at construction if you set
either to `True`, with a message naming the class.
* **Four implementations need extra packages.** `VertexAiCodeExecutor`,
* **Five implementations need extra packages.** `VertexAiCodeExecutor`,
`ContainerCodeExecutor`, `GkeCodeExecutor` and
`AgentEngineSandboxCodeExecutor` arrive with
`pip install "google-adk[extensions]"`. Without them the import still
succeeds, and construction is where it fails.
succeeds, and construction is where it fails. `SmolCodeExecutor` instead
uses `pip install "google-adk[smol]"`; its SDK is loaded on execution.

## Related samples

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ optional-dependencies.all = [
"redis>=4.2",
"rouge-score>=0.1.2",
"slack-bolt>=1.22",
"smolmachines>=1.25.2,<2; sys_platform!='win32'",
"sqlalchemy[asyncio]>=2,<3",
"sqlalchemy-spanner>=1.14",
"tabulate>=0.9",
Expand Down Expand Up @@ -314,6 +315,9 @@ optional-dependencies.slack = [
"aiohttp!=3.14.2",
"slack-bolt>=1.22",
]
optional-dependencies.smol = [
"smolmachines>=1.25.2,<2; sys_platform!='win32'",
]
optional-dependencies.test = [
"a2a-sdk>=0.3.4,<2",
"anthropic>=0.78", # For anthropic model tests; 0.78 introduced ThinkingConfigAdaptiveParam (required for Claude Opus 4.7).
Expand Down
6 changes: 6 additions & 0 deletions src/google/adk/code_executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .code_executor_context import CodeExecutorContext
from .container_code_executor import ContainerCodeExecutor
from .gke_code_executor import GkeCodeExecutor
from .smol_code_executor import SmolCodeExecutor
from .vertex_ai_code_executor import VertexAiCodeExecutor

logger = logging.getLogger('google_adk.' + __name__)
Expand All @@ -38,6 +39,7 @@
'VertexAiCodeExecutor',
'ContainerCodeExecutor',
'GkeCodeExecutor',
'SmolCodeExecutor',
'AgentEngineSandboxCodeExecutor',
]

Expand Down Expand Up @@ -81,6 +83,10 @@ def __getattr__(name: str) -> object:
'GkeCodeExecutor requires additional dependencies. '
'Please install with: pip install "google-adk[extensions]"'
) from e
elif name == 'SmolCodeExecutor':
from .smol_code_executor import SmolCodeExecutor

return SmolCodeExecutor
elif name == 'AgentEngineSandboxCodeExecutor':
try:
from .agent_engine_sandbox_code_executor import AgentEngineSandboxCodeExecutor
Expand Down
149 changes: 149 additions & 0 deletions src/google/adk/code_executors/smol_code_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2026 Google LLC
#
# 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.

"""Run model-generated Python in a disposable local or Cloud microVM."""

from __future__ import annotations

import logging
from typing import Any
from typing import Literal
from typing import TYPE_CHECKING
import uuid

from pydantic import Field
from pydantic import SecretStr
from typing_extensions import override

from .base_code_executor import BaseCodeExecutor
from .code_execution_utils import CodeExecutionInput
from .code_execution_utils import CodeExecutionResult

if TYPE_CHECKING:
from ..agents.invocation_context import InvocationContext

logger = logging.getLogger('google_adk.' + __name__)


class SmolCodeExecutor(BaseCodeExecutor):
"""Execute Python in a fresh Smol Machines microVM for each code block.

A local VM needs Linux KVM or macOS Hypervisor.framework. Use ``target='cloud'``
for Smol Cloud, with ``SMOL_CLOUD_TOKEN`` or a configured Smol CLI login.
Install the optional SDK with ``pip install 'google-adk[smol]'``.

Guest network access is disabled by default. The VM is deleted after every
execution, including failed or timed-out executions; no Python variables or
files persist between blocks. Input files are available in ``/workspace``.
"""

target: Literal['local', 'cloud'] = 'local'
image: str = 'python:3.12-slim'
network_enabled: bool = False
cpus: int = Field(default=2, gt=0)
memory_mb: int = Field(default=1024, gt=0)
api_key: SecretStr | None = Field(default=None, repr=False)
timeout_seconds: int = Field(default=300, gt=0)
stateful: bool = Field(default=False, frozen=True)
optimize_data_file: bool = False

def __init__(self, **data: Any) -> None:
if data.get('stateful'):
raise ValueError('SmolCodeExecutor creates a new VM for each execution.')
super().__init__(**data)

@override
def execute_code(
self,
invocation_context: InvocationContext,
code_execution_input: CodeExecutionInput,
) -> CodeExecutionResult:
del invocation_context # A fresh VM has no session state to share.
for file in code_execution_input.input_files:
if (
not file.name
or file.name in ('.', '..')
or '/' in file.name
or '\\' in file.name
or '\x00' in file.name
):
raise ValueError(f'Invalid sandbox input file name: {file.name!r}')
try:
import smol
except ImportError as exc:
raise ImportError(
'SmolCodeExecutor requires the Smol Machines SDK. '
"Install with: pip install 'google-adk[smol]'"
) from exc

connection = smol.ConnectOptions(
target=self.target,
api_key=self.api_key.get_secret_value() if self.api_key else None,
)
config = smol.MachineConfig(
image=self.image,
resources=smol.ResourceSpec(
cpus=self.cpus,
memory_mb=self.memory_mb,
network=self.network_enabled,
),
persistent=False,
auto_stop_seconds=(
max(600, self.timeout_seconds + 60)
if self.target == 'cloud'
else None
),
ttl_seconds=(
max(1200, 2 * self.timeout_seconds + 60)
if self.target == 'cloud'
else None
),
)
machine = smol.Machine.create(config, connection)
failed = False
try:
script_path = f'/workspace/adk-code-{uuid.uuid4().hex}.py'
for file in code_execution_input.input_files:
content = (
file.content.encode('utf-8')
if isinstance(file.content, str)
else file.content
)
machine.write_file(f'/workspace/{file.name}', content)
machine.write_file(script_path, code_execution_input.code)
result = machine.exec(
['python3', script_path],
smol.ExecOptions(
workdir='/workspace', timeout=float(self.timeout_seconds)
),
)
stderr = result.stderr
if result.stdout_truncated or result.stderr_truncated:
stderr += '\nSmol VM output was truncated.'
return CodeExecutionResult(
stdout=result.stdout,
stderr=stderr,
exit_code=result.exit_code,
)
except BaseException:
failed = True
raise
finally:
try:
machine.delete()
except Exception:
if failed:
logger.exception('Could not delete Smol VM after execution failure')
else:
raise
Loading
Loading