adk-code-mode 0.4.1__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adk_code_mode-1.0.0/.release-please-manifest.json +3 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/CHANGELOG.md +11 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/PKG-INFO +56 -111
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/README.md +53 -108
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/pyproject.toml +2 -2
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/__about__.py +1 -1
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/__init__.py +2 -9
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/_artifact_tools.py +1 -1
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/output.py +4 -3
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/runtime/base.py +9 -8
- adk_code_mode-0.4.1/src/adk_code_mode/executor.py → adk_code_mode-1.0.0/src/adk_code_mode/tool.py +373 -433
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tool_result_artifacts.py +5 -5
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/catalog.py +3 -24
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/normaliser.py +1 -1
- adk_code_mode-1.0.0/src/adk_code_mode/workspace/__init__.py +8 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/workspace/files.py +1 -5
- adk_code_mode-1.0.0/tests/conftest.py +28 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_agent_with_fake_llm.py +85 -33
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_catalog.py +1 -13
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_docker_integration.py +22 -24
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_host_loop.py +2 -2
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_integration.py +164 -242
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_output.py +2 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_remote_integration.py +50 -55
- adk_code_mode-1.0.0/tests/test_tool.py +166 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_tool_result_artifacts.py +20 -17
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_workspace_files.py +0 -5
- adk_code_mode-0.4.1/.release-please-manifest.json +0 -3
- adk_code_mode-0.4.1/src/adk_code_mode/callback.py +0 -81
- adk_code_mode-0.4.1/src/adk_code_mode/workspace/__init__.py +0 -8
- adk_code_mode-0.4.1/tests/conftest.py +0 -37
- adk_code_mode-0.4.1/tests/test_callback.py +0 -147
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.githooks/pre-commit +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.githooks/pre-push +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.gitignore +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.prettierrc +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.vscode/settings.json +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/.zed/settings.json +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/AGENTS.md +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/CLAUDE.md +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/LICENSE +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/Makefile +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/docker/Dockerfile +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/release-please-config.json +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/runtime/docker.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/runtime/protocol.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/runtime/remote.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/namespacing.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/src/adk_code_mode/tools/stubs.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/__init__.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/_fake_runtime.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_artifact_tools.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_dispatcher.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_docker_image.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_namespacing.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_normaliser.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_protocol.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_remote_backend.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_stubs.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/tests/test_version.py +0 -0
- {adk_code_mode-0.4.1 → adk_code_mode-1.0.0}/uv.lock +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.4.1...adk-code-mode-v1.0.0) (2026-07-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* `CodeModeCodeExecutor`, `code_mode_before_model_callback`, and `CODE_MODE_SYSTEM_INSTRUCTION` no longer exist. Add `ExecuteCodeTool` to the agent's `tools=[...]` instead of wiring `code_executor=`; drop `before_model_callback=`, `generate_content_config=`, and `CODE_MODE_SYSTEM_INSTRUCTION` entirely; `release_invocation` is now `async`. See the README's migration section.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* replace `CodeModeCodeExecutor` with `ExecuteCodeTool` ([#10](https://github.com/a2anet/adk-code-mode/issues/10)) ([b78572e](https://github.com/a2anet/adk-code-mode/commit/b78572e72446930548760756255390d1a3cc388a))
|
|
13
|
+
|
|
3
14
|
## [0.4.1](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.4.0...adk-code-mode-v0.4.1) (2026-07-10)
|
|
4
15
|
|
|
5
16
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A "Code Mode"
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A "Code Mode" sandboxed code-execution tool for ADK agents to interact with tools, files, and custom packages with Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/A2ANet/adk-code-mode#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/A2ANet/adk-code-mode/issues
|
|
7
7
|
Project-URL: Source, https://github.com/A2ANet/adk-code-mode
|
|
8
8
|
Author-email: A2A Net <hello@a2anet.com>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Keywords: a2a,adk,agents,code-
|
|
11
|
+
Keywords: a2a,adk,agents,code-mode,execute-code
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -27,12 +27,12 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
|
|
28
28
|
# ADK Code Mode
|
|
29
29
|
|
|
30
|
-
A [Code Mode](https://blog.cloudflare.com/code-mode/) code
|
|
30
|
+
A [Code Mode](https://blog.cloudflare.com/code-mode/) sandboxed code-execution tool for [Agent Development Kit (ADK)](https://github.com/google/adk-python).
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
`ExecuteCodeTool` lets ADK agents write Python code to call tools and read and write files.
|
|
33
33
|
Code runs inside a sandboxed container, and tools (and their credentials) are executed on the host.
|
|
34
34
|
The base image comes with the stdlib and can be extended with any Python package you want.
|
|
35
|
-
|
|
35
|
+
The sandboxed container can list, load, and save ADK Artifacts, and files it creates are returned as artifacts.
|
|
36
36
|
|
|
37
37
|
Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and Anthropic's [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).
|
|
38
38
|
|
|
@@ -40,22 +40,22 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
40
40
|
|
|
41
41
|
- **Call ADK tools from sandbox code** — imports against the `tools` package proxy back to the host and run through ADK's `before_tool` / `after_tool` / `on_error` callbacks and the plugin manager exactly as direct tool calls would.
|
|
42
42
|
- **Bake any Python package into the image** — extend the published base image with anything the model's code needs to `import`, no runtime `pip install` required.
|
|
43
|
-
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
43
|
+
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`. Files the code creates or changes are saved as artifacts automatically too.
|
|
44
44
|
- **Tool results saved as artifacts** — on by default; every tool's result is persisted as a `code_mode.tool_result` artifact (with optional model-supplied name/description) so hosts can forward outputs and large results stay out of the prompt. Opt out with `save_tool_results_as_artifacts=False`.
|
|
45
45
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
46
|
-
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's
|
|
46
|
+
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's `execute_code` calls. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
|
|
47
47
|
- **Local development** — `UnsafeLocalDockerBackend` runs the sandbox against your local Docker daemon for fast iteration. **Not for production** — see [Safety](#-safety).
|
|
48
48
|
|
|
49
49
|
| | BuiltIn | AgentEngineSandbox | VertexAi | Container | Gke | CodeMode |
|
|
50
|
-
| ----------------------------------- | ------- |
|
|
51
|
-
| Call ADK tools from code | no | no
|
|
52
|
-
| Extra Python packages | no | no (more than stdlib but fixed)
|
|
53
|
-
| Variables are stateful | no | yes
|
|
54
|
-
| Input files | no | yes
|
|
55
|
-
| Output files | no | yes
|
|
56
|
-
| Storage | no | yes (via variables)
|
|
57
|
-
| Local development version available | no | no
|
|
58
|
-
| Bounded stdout/stderr | no | no
|
|
50
|
+
| ----------------------------------- | ------- | -------------------------------- | -------------------------------- | --------- | --- | ------------------------ |
|
|
51
|
+
| Call ADK tools from code | no | no | no | no | no | yes (with limitations) |
|
|
52
|
+
| Extra Python packages | no | no (more than stdlib but fixed) | no (more than stdlib but fixed) | yes | yes | yes |
|
|
53
|
+
| Variables are stateful | no | yes | yes | no | no | yes (within a turn) |
|
|
54
|
+
| Input files | no | yes | yes | no | no | no (use Artifacts) |
|
|
55
|
+
| Output files | no | yes | yes | no | no | yes (as Artifacts) |
|
|
56
|
+
| Storage | no | yes (via variables) | yes (via variables) | no | no | yes (via ADK Artifacts) |
|
|
57
|
+
| Local development version available | no | no | no | yes | yes | yes |
|
|
58
|
+
| Bounded stdout/stderr | no | no | no | no | no | yes (`max_output_chars`) |
|
|
59
59
|
|
|
60
60
|
## 📦 Install
|
|
61
61
|
|
|
@@ -79,26 +79,15 @@ Requires Python 3.10+. Local development requires [Docker](https://docs.docker.c
|
|
|
79
79
|
|
|
80
80
|
## 🚀 Usage
|
|
81
81
|
|
|
82
|
-
Build
|
|
83
|
-
|
|
84
|
-
- **`CODE_MODE_SYSTEM_INSTRUCTION`** — append to the agent's `instruction`. Teaches the model how to write code blocks and use artifacts.
|
|
85
|
-
- **`code_mode_before_model_callback`** — set as `before_model_callback`. Injects the tool catalog (`<code-mode>` block) into the system prompt on every model turn.
|
|
86
|
-
- **`generate_content_config`** with `function_calling_config.mode="NONE"` — disables native function calling so the model writes Python instead of attempting tool calls that fail with `MALFORMED_FUNCTION_CALL` (since `tools=[]`).
|
|
87
|
-
- **`release_invocation`** — call it from the agent's `after_agent_callback` to release the turn's sandbox container as soon as the turn ends. An idle reaper (`session_idle_timeout_seconds`, default `600`) is a backstop, so containers are still reclaimed without it — just later.
|
|
82
|
+
Build an `ExecuteCodeTool`, give it the tools the sandboxed code may call, and attach it to your agent. Wire `release_invocation` into `after_agent_callback` to release the turn's sandbox container when the turn ends. An idle reaper (`session_idle_timeout_seconds`, default `600`) is a backstop.
|
|
88
83
|
|
|
89
84
|
### Production (remote sandbox)
|
|
90
85
|
|
|
91
86
|
```python
|
|
92
87
|
from google.adk.agents import LlmAgent
|
|
93
|
-
from
|
|
94
|
-
from adk_code_mode import (
|
|
95
|
-
CODE_MODE_SYSTEM_INSTRUCTION,
|
|
96
|
-
CodeModeCodeExecutor,
|
|
97
|
-
RemoteBackend,
|
|
98
|
-
code_mode_before_model_callback,
|
|
99
|
-
)
|
|
88
|
+
from adk_code_mode import ExecuteCodeTool, RemoteBackend
|
|
100
89
|
|
|
101
|
-
|
|
90
|
+
tool = ExecuteCodeTool(
|
|
102
91
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
103
92
|
backend=RemoteBackend(
|
|
104
93
|
url="https://sandbox-xyz.run.app", # your deployed sandbox URL
|
|
@@ -106,21 +95,14 @@ executor = CodeModeCodeExecutor(
|
|
|
106
95
|
),
|
|
107
96
|
)
|
|
108
97
|
|
|
109
|
-
def _release_sandbox(callback_context):
|
|
110
|
-
|
|
98
|
+
async def _release_sandbox(callback_context):
|
|
99
|
+
await tool.release_invocation(callback_context.invocation_id)
|
|
111
100
|
|
|
112
101
|
root_agent = LlmAgent(
|
|
113
102
|
name="assistant",
|
|
114
|
-
model="gemini-
|
|
115
|
-
instruction=
|
|
116
|
-
tools=[],
|
|
117
|
-
code_executor=executor,
|
|
118
|
-
generate_content_config=genai_types.GenerateContentConfig(
|
|
119
|
-
tool_config=genai_types.ToolConfig(
|
|
120
|
-
function_calling_config=genai_types.FunctionCallingConfig(mode="NONE"),
|
|
121
|
-
),
|
|
122
|
-
),
|
|
123
|
-
before_model_callback=code_mode_before_model_callback(executor),
|
|
103
|
+
model="gemini-3.5-flash",
|
|
104
|
+
instruction="You are a helpful assistant.",
|
|
105
|
+
tools=[tool],
|
|
124
106
|
after_agent_callback=[_release_sandbox],
|
|
125
107
|
)
|
|
126
108
|
```
|
|
@@ -130,14 +112,9 @@ root_agent = LlmAgent(
|
|
|
130
112
|
> **`UnsafeLocalDockerBackend` is not safe for production or multi-tenant use.** See [Safety](#-safety).
|
|
131
113
|
|
|
132
114
|
```python
|
|
133
|
-
from adk_code_mode import
|
|
134
|
-
CODE_MODE_SYSTEM_INSTRUCTION,
|
|
135
|
-
CodeModeCodeExecutor,
|
|
136
|
-
UnsafeLocalDockerBackend,
|
|
137
|
-
code_mode_before_model_callback,
|
|
138
|
-
)
|
|
115
|
+
from adk_code_mode import ExecuteCodeTool, UnsafeLocalDockerBackend
|
|
139
116
|
|
|
140
|
-
|
|
117
|
+
tool = ExecuteCodeTool(
|
|
141
118
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
142
119
|
backend=UnsafeLocalDockerBackend(image="ghcr.io/a2anet/adk-code-mode:latest"),
|
|
143
120
|
)
|
|
@@ -217,9 +194,9 @@ RemoteBackend(
|
|
|
217
194
|
|
|
218
195
|
The same pattern works on any platform that runs Docker containers as HTTP services (AWS Fargate/ECS, Azure Container Instances, Kubernetes, Fly.io, etc.):
|
|
219
196
|
|
|
220
|
-
1. **One container per turn.** Each container handles exactly one turn (one or more
|
|
197
|
+
1. **One container per turn.** Each container handles exactly one turn (one or more `execute_code` calls) and exits.
|
|
221
198
|
2. **Block all outbound network access.** Without egress restrictions, user code can exfiltrate data, access cloud metadata endpoints, or scan internal networks.
|
|
222
|
-
3. **Keep `/workspace` and `/tools` writable.** The sandbox stages
|
|
199
|
+
3. **Keep `/workspace` and `/tools` writable.** The sandbox stages the working directory and materialises the `tools` package into `/tools` at connect time. If you set a read-only root filesystem (e.g., `readOnlyRootFilesystem: true` in Kubernetes), mount both as writable volumes (e.g., an `emptyDir`).
|
|
223
200
|
4. **Authenticate connections.** Set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth (IAM, NetworkPolicy, security groups) on top.
|
|
224
201
|
|
|
225
202
|
Required env vars:
|
|
@@ -252,9 +229,9 @@ RemoteBackend(
|
|
|
252
229
|
|
|
253
230
|
Code Mode exposes two file surfaces:
|
|
254
231
|
|
|
255
|
-
-
|
|
232
|
+
- **The working directory** — the turn's workspace. It persists across the turn's `execute_code` calls and resets between turns. Files created or changed by a call are collected afterward and saved as session artifacts automatically, returned to the model as a list of filenames (reloadable via `load_artifact`) — nothing is re-hydrated into the working directory on the next turn unless the model explicitly loads it back with `load_artifact`.
|
|
256
233
|
|
|
257
|
-
- **ADK Artifacts** — persistent cross-turn storage. `
|
|
234
|
+
- **ADK Artifacts** — persistent cross-turn storage. `ExecuteCodeTool` injects three tools into the sandbox:
|
|
258
235
|
|
|
259
236
|
```python
|
|
260
237
|
import json
|
|
@@ -275,10 +252,11 @@ Pass `include_artifact_tools=False` to opt out. To react when the model saves an
|
|
|
275
252
|
|
|
276
253
|
```python
|
|
277
254
|
async def on_saved(invocation_context, delta):
|
|
278
|
-
# delta is {filename: version} for everything
|
|
255
|
+
# delta is {filename: version} for everything the sandbox-side save_artifact
|
|
256
|
+
# calls (or wrapped tool results) saved this turn.
|
|
279
257
|
...
|
|
280
258
|
|
|
281
|
-
|
|
259
|
+
ExecuteCodeTool(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
282
260
|
```
|
|
283
261
|
|
|
284
262
|
### Tool results as artifacts
|
|
@@ -288,7 +266,7 @@ By default (`save_tool_results_as_artifacts=True`) every non-artifact tool is wr
|
|
|
288
266
|
Naming is transparent: the filename is derived from the tool name and call id. Each wrapped tool also gains two **optional** parameters — `artifact_name` and `artifact_description` — that the model may pass to name/describe the saved artifact; both land in the artifact's `custom_metadata` (`code_mode.artifact_name` / `code_mode.artifact_description`) alongside the marker. Set `save_tool_results_as_artifacts=False` to return tool results inline without persisting them.
|
|
289
267
|
|
|
290
268
|
```python
|
|
291
|
-
|
|
269
|
+
ExecuteCodeTool(tools=..., backend=..., save_tool_results_as_artifacts=True)
|
|
292
270
|
```
|
|
293
271
|
|
|
294
272
|
## 🐳 Sandbox Image
|
|
@@ -304,80 +282,52 @@ The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To
|
|
|
304
282
|
|
|
305
283
|
## ⚙️ Configuration
|
|
306
284
|
|
|
307
|
-
All settings are `
|
|
285
|
+
All settings are `ExecuteCodeTool` constructor arguments:
|
|
308
286
|
|
|
309
287
|
| Argument | Default | Purpose |
|
|
310
288
|
| --- | --- | --- |
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
289
|
+
| `append_function_stubs_to_system_instruction` | `True` | Appends a `<code-mode>` block listing available function signatures and docstrings to the system instruction on every model turn. If the rendered catalog would exceed `max_catalog_chars`, the model can still discover functions by listing `/tools/` and reading the generated stubs. |
|
|
290
|
+
| `max_catalog_chars` | `50_000` | Maximum size of the appended function catalog. |
|
|
291
|
+
| `max_output_chars` | `50_000` | Caps stdout/stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<call-id>.txt` and the model sees a head-and-tail view pointing to it. |
|
|
313
292
|
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
314
|
-
| `timeout_seconds` | `None` | Caps overall execution time. Defaults to the platform request timeout (e.g. Cloud Run `--timeout`); set explicitly for defense in depth. |
|
|
315
|
-
| `per_tool_timeout_seconds` | `None` | Caps each individual tool call. |
|
|
293
|
+
| `timeout_seconds` | `None` | Caps overall execution time of one `execute_code` call. Defaults to the platform request timeout (e.g. Cloud Run `--timeout`); set explicitly for defense in depth. |
|
|
294
|
+
| `per_tool_timeout_seconds` | `None` | Caps each individual tool call made from within the sandbox. |
|
|
316
295
|
| `session_idle_timeout_seconds` | `600` | Idle reaper: closes a turn's container once it goes untouched this long. Backstop for turns that never call `release_invocation`. |
|
|
317
296
|
|
|
318
297
|
The model can read spilled stdout back from the overflow artifact:
|
|
319
298
|
|
|
320
299
|
```python
|
|
321
300
|
from tools import load_artifact
|
|
322
|
-
spilled = load_artifact(filename="code_mode/stdout/<
|
|
301
|
+
spilled = load_artifact(filename="code_mode/stdout/<call-id>.txt")
|
|
323
302
|
print(spilled["data"][-2000:])
|
|
324
303
|
```
|
|
325
304
|
|
|
326
305
|
### Turn-scoped sessions
|
|
327
306
|
|
|
328
|
-
A sandbox container is held open for one **turn** (one ADK invocation) and reused across that turn's
|
|
307
|
+
A sandbox container is held open for one **turn** (one ADK invocation) and reused across that turn's `execute_code` calls, so cold start is paid at most once per turn. Python globals **and** the working directory persist across a turn's calls and reset between turns — use ADK Artifacts for cross-turn persistence. The container is released when the turn ends (via `await tool.release_invocation(...)`, wired in [Usage](#-usage)) and destroyed by the platform, so no state survives into another turn or tenant.
|
|
329
308
|
|
|
330
309
|
## 🏗️ Architecture
|
|
331
310
|
|
|
332
|
-
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`.
|
|
311
|
+
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. `ExecuteCodeTool.process_llm_request` resolves tools and, when `append_function_stubs_to_system_instruction` is enabled, renders the catalog and appends it to the system instruction. At execution time (`run_async`), it generates a `tools/` Python package of thin stubs, stages the working directory, and opens (or reuses) the turn's sandbox connection — the container spans the whole turn.
|
|
333
312
|
|
|
334
313
|
**Sandbox wheel (`adk-code-mode-sandbox`).** Pre-installed in the container image. When model code calls a stub, it sends a JSON-Lines frame over the control connection; the host runs the real tool (with callbacks and plugins) and sends the result back.
|
|
335
314
|
|
|
336
315
|
The only things crossing the boundary are: code, tool call arguments, tool return values, and log frames.
|
|
337
316
|
|
|
338
317
|
| Backend | Transport | Multi-tenant safe? | When to use |
|
|
339
|
-
| -------------------------- | ---------------------- |
|
|
318
|
+
| -------------------------- | ---------------------- | ------------------- | -------------------------------- |
|
|
340
319
|
| `RemoteBackend` | WebSocket over HTTPS | **Yes** | Production — any cloud platform |
|
|
341
|
-
| `UnsafeLocalDockerBackend` | TCP over Docker bridge | No
|
|
320
|
+
| `UnsafeLocalDockerBackend` | TCP over Docker bridge | No | Local development only |
|
|
342
321
|
|
|
343
322
|
### What the model sees
|
|
344
323
|
|
|
345
|
-
|
|
324
|
+
`execute_code` has a single `code: string` parameter. With `append_function_stubs_to_system_instruction` enabled, the system instruction also gets a `<code-mode>` reference catalog on every turn:
|
|
346
325
|
|
|
347
326
|
~~~
|
|
348
327
|
…your instruction…
|
|
349
328
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
The Python Standard Library and a set of custom tools are available to you as an importable library, listed in the `<code-mode>` section below.
|
|
353
|
-
To see the result of your code, you need to print it. For example, if you had the following tool:
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
<code-mode>
|
|
357
|
-
|
|
358
|
-
from tools.slack import send_message
|
|
359
|
-
|
|
360
|
-
def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
|
|
361
|
-
"""Send a message to a Slack channel."""
|
|
362
|
-
...
|
|
363
|
-
|
|
364
|
-
</code-mode>
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
To call the tool, you should write:
|
|
368
|
-
|
|
369
|
-
````
|
|
370
|
-
```python
|
|
371
|
-
from tools.slack import send_message
|
|
372
|
-
|
|
373
|
-
print(send_message(channel="C123", text="hi"))
|
|
374
|
-
```
|
|
375
|
-
````
|
|
376
|
-
|
|
377
|
-
# How to use files and variables in between executions
|
|
378
|
-
You can reuse variables and files within a turn. Between turns, to reuse variables and files you must save and load Artifacts.
|
|
379
|
-
To list available Artifacts, use the `list_artifacts` tool, to save an Artifact, use the `save_artifact` tool, and to load an Artifact, use the `load_artifact` tool.
|
|
380
|
-
Tool results are automatically saved as Artifacts.
|
|
329
|
+
Reference catalog of the Python functions available inside the execute_code sandbox.
|
|
330
|
+
Import these functions from the `tools` package in the code you pass to execute_code.
|
|
381
331
|
|
|
382
332
|
<code-mode>
|
|
383
333
|
|
|
@@ -403,17 +353,12 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
403
353
|
|
|
404
354
|
With `save_tool_results_as_artifacts` enabled (the default), each non-artifact tool above — e.g. `list_channels` and `send_message` — also carries two optional `artifact_name: str | None = ...` / `artifact_description: str | None = ...` parameters for naming its saved result.
|
|
405
355
|
|
|
406
|
-
|
|
356
|
+
If the rendered catalog would exceed `max_catalog_chars`, nothing is appended to the system instruction that turn — not even a fallback note. The model can still navigate the sandbox from Python:
|
|
407
357
|
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
(a top-level tool, importable as `from tools import <name>`) or a
|
|
413
|
-
subdirectory (a namespace, with tools importable as
|
|
414
|
-
`from tools.<namespace> import <name>`). To see a tool's signature and
|
|
415
|
-
docstring, read its `.py` file with `open(...).read()`.
|
|
416
|
-
</code-mode>
|
|
358
|
+
```python
|
|
359
|
+
import pathlib
|
|
360
|
+
print(list(pathlib.Path("/tools").iterdir()))
|
|
361
|
+
print(open("/tools/slack/send_message.py").read()) # signature + docstring
|
|
417
362
|
```
|
|
418
363
|
|
|
419
364
|
Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns `{"kind": "text" | "bytes", "data": str, "mime_type": str | None}`.
|
|
@@ -424,7 +369,7 @@ Text and JSON-like MIME types travel as plain strings in artifact tools; binary
|
|
|
424
369
|
|
|
425
370
|
`RemoteBackend` is designed for multi-tenant production use where untrusted users submit arbitrary Python code:
|
|
426
371
|
|
|
427
|
-
- **One container per turn (one tenant, one invocation).** Within a turn the process/filesystem are reused across that turn's
|
|
372
|
+
- **One container per turn (one tenant, one invocation).** Within a turn the process/filesystem are reused across that turn's `execute_code` calls; the container is destroyed at turn end, with **no cross-turn or cross-tenant sharing**.
|
|
428
373
|
- **Environment sanitization.** All env vars are stripped except a safe allowlist (`PATH`, `HOME`, `USER`, locale vars, Python config) before user code runs.
|
|
429
374
|
- **Credentials never enter the sandbox.** API keys, OAuth tokens, and connection strings stay in the host process. The container only receives tool results.
|
|
430
375
|
- **Bearer token authentication.** WebSocket connections without a valid token are rejected. Always set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth on top.
|
|
@@ -449,7 +394,7 @@ Named "Unsafe" intentionally: it binds a TCP listener on `0.0.0.0`, communicates
|
|
|
449
394
|
## ⚠️ Limitations
|
|
450
395
|
|
|
451
396
|
- **No credential-requesting tools.** Tools that need ADK to request credentials, confirmations, UI widgets, agent transfer, escalation, or that yield without an immediate response are rejected with a structured error.
|
|
452
|
-
- **State is turn-scoped.** Variables and
|
|
397
|
+
- **State is turn-scoped.** Variables and the working directory persist across `execute_code` calls **within** a turn, but reset between turns. Use `save_artifact` / `load_artifact` to persist across turns.
|
|
453
398
|
- **No runtime package installation.** The sandbox ships with the Python Standard Library and the runtime's own dependencies only. Extra packages must be baked into the image at build time.
|
|
454
399
|
|
|
455
400
|
## 🛠️ Development
|