adk-code-mode 0.3.0__tar.gz → 0.4.1__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-0.4.1/.release-please-manifest.json +3 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/CHANGELOG.md +27 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/PKG-INFO +52 -86
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/README.md +51 -85
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/docker/Dockerfile +2 -2
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +8 -6
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/__about__.py +1 -1
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/__init__.py +14 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/callback.py +1 -1
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/executor.py +46 -10
- adk_code_mode-0.4.1/src/adk_code_mode/tool_result_artifacts.py +187 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_remote_integration.py +5 -1
- adk_code_mode-0.4.1/tests/test_tool_result_artifacts.py +177 -0
- adk_code_mode-0.3.0/.release-please-manifest.json +0 -3
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.githooks/pre-commit +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.githooks/pre-push +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.gitignore +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.prettierrc +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.vscode/settings.json +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/.zed/settings.json +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/AGENTS.md +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/CLAUDE.md +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/LICENSE +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/Makefile +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/pyproject.toml +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/release-please-config.json +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/_artifact_tools.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/output.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/runtime/base.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/runtime/docker.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/runtime/protocol.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/runtime/remote.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/catalog.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/namespacing.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/normaliser.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/tools/stubs.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/workspace/__init__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/src/adk_code_mode/workspace/files.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/__init__.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/_fake_runtime.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/conftest.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_agent_with_fake_llm.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_artifact_tools.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_callback.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_catalog.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_dispatcher.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_docker_image.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_docker_integration.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_host_loop.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_integration.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_namespacing.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_normaliser.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_output.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_protocol.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_remote_backend.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_stubs.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_version.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/tests/test_workspace_files.py +0 -0
- {adk_code_mode-0.3.0 → adk_code_mode-0.4.1}/uv.lock +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [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
|
+
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
|
|
8
|
+
* note the sandbox writes to `/tools` in remote deployment guide ([96bb45e](https://github.com/a2anet/adk-code-mode/commit/96bb45e4e18d9d54709f4c10b6b09cf3567ee385))
|
|
9
|
+
|
|
10
|
+
## [0.4.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.3.0...adk-code-mode-v0.4.0) (2026-07-09)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* save tool results as artifacts with optional model-supplied naming ([76a3671](https://github.com/a2anet/adk-code-mode/commit/76a36713b2c52bd5b8dc51d3669226a47e7108e5))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* align code-mode example with the `<code-mode>` catalog and fix fences ([4a0cc6d](https://github.com/a2anet/adk-code-mode/commit/4a0cc6d0fc99d5425f6682cc489e76c9ddef7d46))
|
|
21
|
+
* clarify code-mode tools are a Python library, not function tools ([48d1018](https://github.com/a2anet/adk-code-mode/commit/48d1018152a2ed2a8e7361de6ce0f43b31b429ce))
|
|
22
|
+
* nudge the model to call tools via Python in the code-mode block ([141baf3](https://github.com/a2anet/adk-code-mode/commit/141baf3a44976e82cf437b2064666be893d6d409))
|
|
23
|
+
* update `CODE_MODE_SYSTEM_INSTRUCTION` and `README.md` ([933d42c](https://github.com/a2anet/adk-code-mode/commit/933d42cb237b9d6deeb6611511cba28a0d5e9261))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
* streamline `README.md` and fix broken code fences ([c237514](https://github.com/a2anet/adk-code-mode/commit/c2375149df48e21318967090b6c85d0b1f411ea2))
|
|
29
|
+
|
|
3
30
|
## [0.3.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.2.2...adk-code-mode-v0.3.0) (2026-07-06)
|
|
4
31
|
|
|
5
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A "Code Mode" Code Executor for ADK for 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
|
|
@@ -41,6 +41,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
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
43
|
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
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`.
|
|
44
45
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
45
46
|
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's code blocks. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
|
|
46
47
|
- **Local development** — `UnsafeLocalDockerBackend` runs the sandbox against your local Docker daemon for fast iteration. **Not for production** — see [Safety](#-safety).
|
|
@@ -78,11 +79,12 @@ Requires Python 3.10+. Local development requires [Docker](https://docs.docker.c
|
|
|
78
79
|
|
|
79
80
|
## 🚀 Usage
|
|
80
81
|
|
|
81
|
-
Build a `CodeModeCodeExecutor`, then wire
|
|
82
|
+
Build a `CodeModeCodeExecutor`, then wire four things into the agent:
|
|
82
83
|
|
|
83
84
|
- **`CODE_MODE_SYSTEM_INSTRUCTION`** — append to the agent's `instruction`. Teaches the model how to write code blocks and use artifacts.
|
|
84
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.
|
|
85
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.
|
|
86
88
|
|
|
87
89
|
### Production (remote sandbox)
|
|
88
90
|
|
|
@@ -104,6 +106,9 @@ executor = CodeModeCodeExecutor(
|
|
|
104
106
|
),
|
|
105
107
|
)
|
|
106
108
|
|
|
109
|
+
def _release_sandbox(callback_context):
|
|
110
|
+
executor.release_invocation(callback_context.invocation_id)
|
|
111
|
+
|
|
107
112
|
root_agent = LlmAgent(
|
|
108
113
|
name="assistant",
|
|
109
114
|
model="gemini-2.5-pro",
|
|
@@ -116,6 +121,7 @@ root_agent = LlmAgent(
|
|
|
116
121
|
),
|
|
117
122
|
),
|
|
118
123
|
before_model_callback=code_mode_before_model_callback(executor),
|
|
124
|
+
after_agent_callback=[_release_sandbox],
|
|
119
125
|
)
|
|
120
126
|
```
|
|
121
127
|
|
|
@@ -146,16 +152,7 @@ print(send_message(channel="C123", text="hi"))
|
|
|
146
152
|
|
|
147
153
|
## 🌐 Remote Deployment
|
|
148
154
|
|
|
149
|
-
**Every turn runs in its own container
|
|
150
|
-
|
|
151
|
-
Setting `ADK_CODE_MODE_CONTROL_HTTP=1` activates HTTP mode. The container:
|
|
152
|
-
|
|
153
|
-
1. Starts a WebSocket server on port 8080 (configurable via `PORT`)
|
|
154
|
-
2. Accepts exactly one connection (rejects further connections with 503)
|
|
155
|
-
3. Receives the tools tar **once**, then announces readiness
|
|
156
|
-
4. Sanitizes the environment (strips all env vars except a safe allowlist)
|
|
157
|
-
5. For each code block: receives that block's workspace tar, executes user code (tools proxied back to the host over the same WebSocket; globals + `/workspace` persist across blocks), then returns stdout/stderr and the updated workspace
|
|
158
|
-
6. Exits when the turn ends (`ShutdownFrame` or disconnect)
|
|
155
|
+
**Every turn runs in its own container**, which the platform destroys when the turn ends — no cross-turn or cross-tenant state. The sandbox runs as a WebSocket server (set `ADK_CODE_MODE_CONTROL_HTTP=1`) and accepts exactly one connection, so you **must** configure your platform for one container per turn (`--concurrency 1` on Cloud Run, or equivalent).
|
|
159
156
|
|
|
160
157
|
### Deploy to Cloud Run
|
|
161
158
|
|
|
@@ -183,7 +180,7 @@ gcloud compute networks vpc-access connectors create adk-sandbox-connector \
|
|
|
183
180
|
--region=<region> \
|
|
184
181
|
--subnet=adk-sandbox-subnet
|
|
185
182
|
|
|
186
|
-
# Deploy — note --concurrency 1
|
|
183
|
+
# Deploy — note --concurrency 1, --vpc-egress=all-traffic, and the /health startup probe
|
|
187
184
|
gcloud run deploy adk-code-mode-sandbox \
|
|
188
185
|
--image <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest \
|
|
189
186
|
--region <region> \
|
|
@@ -197,10 +194,11 @@ gcloud run deploy adk-code-mode-sandbox \
|
|
|
197
194
|
--vpc-connector=adk-sandbox-connector \
|
|
198
195
|
--vpc-egress=all-traffic \
|
|
199
196
|
--set-env-vars "ADK_CODE_MODE_CONTROL_HTTP=1" \
|
|
200
|
-
--set-secrets "ADK_CODE_MODE_AUTH_TOKEN=<your-secret-name>:latest"
|
|
197
|
+
--set-secrets "ADK_CODE_MODE_AUTH_TOKEN=<your-secret-name>:latest" \
|
|
198
|
+
--startup-probe "httpGet.path=/health,httpGet.port=8080,timeoutSeconds=3,periodSeconds=3,failureThreshold=80"
|
|
201
199
|
```
|
|
202
200
|
|
|
203
|
-
> These flags are **recommendations to tune per deployment**, not hard requirements. `--timeout 3600` (Cloud Run's max) is the per-turn ceiling since the container holds the WebSocket for the whole turn; `--max-instances` should cover your peak concurrent *turns* (`120` covers a 10–100 target — verify your region's Cloud Run vCPU quota).
|
|
201
|
+
> These flags are **recommendations to tune per deployment**, not hard requirements. `--timeout 3600` (Cloud Run's max) is the per-turn ceiling since the container holds the WebSocket for the whole turn; `--max-instances` should cover your peak concurrent *turns* (`120` covers a 10–100 target — verify your region's Cloud Run vCPU quota). The `/health` startup probe avoids cold-start `HTTP 503`s — Cloud Run's default TCP probe opens a raw socket the WebSocket server rejects.
|
|
204
202
|
|
|
205
203
|
Then in your agent:
|
|
206
204
|
|
|
@@ -215,22 +213,20 @@ RemoteBackend(
|
|
|
215
213
|
|
|
216
214
|
> **`--vpc-egress=all-traffic` with a deny-all VPC is critical for security.** Without it, user code can make arbitrary outbound requests — including hitting the GCP metadata endpoint (`169.254.169.254`) to steal the service account token, exfiltrating data, or scanning your VPC. The sandbox only needs to _accept_ inbound connections; it never needs outbound access.
|
|
217
215
|
|
|
218
|
-
> **Configure an HTTP startup probe against `/health`.** Cloud Run's default TCP probe opens a raw socket that the `websockets` server rejects as a malformed HTTP request, which can race with real traffic during cold starts and surface to clients as `HTTP 503` on the WebSocket handshake. Add `--startup-probe="httpGet.path=/health,httpGet.port=8080,timeoutSeconds=3,periodSeconds=3,failureThreshold=80"` (or equivalent) so probes hit the `/health` endpoint the sandbox handles natively. Keep `periodSeconds` small (e.g. `3`): it sets how long Cloud Run waits before re-probing, so a short period lets a ready container start serving in ~3s instead of ~10s — raise `failureThreshold` to keep the same overall startup budget (`periodSeconds × failureThreshold`). Turn-scoping already gives demand-driven warmth (a container stays up for the whole turn), so `--min-instances=0` is fine; only set `--min-instances` if you want to eliminate cold starts at the cost of a paid warm pool.
|
|
219
|
-
|
|
220
216
|
### Deploy on other platforms
|
|
221
217
|
|
|
222
218
|
The same pattern works on any platform that runs Docker containers as HTTP services (AWS Fargate/ECS, Azure Container Instances, Kubernetes, Fly.io, etc.):
|
|
223
219
|
|
|
224
220
|
1. **One container per turn.** Each container handles exactly one turn (one or more code blocks) and exits.
|
|
225
221
|
2. **Block all outbound network access.** Without egress restrictions, user code can exfiltrate data, access cloud metadata endpoints, or scan internal networks.
|
|
226
|
-
3. **
|
|
222
|
+
3. **Keep `/workspace` and `/tools` writable.** The sandbox stages input files into `/workspace` 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`).
|
|
227
223
|
4. **Authenticate connections.** Set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth (IAM, NetworkPolicy, security groups) on top.
|
|
228
224
|
|
|
229
225
|
Required env vars:
|
|
230
226
|
|
|
231
227
|
| Env var | Required | Default | Purpose |
|
|
232
228
|
| ------------------------------------ | -------- | ------- | -------------------------------- |
|
|
233
|
-
| `ADK_CODE_MODE_CONTROL_HTTP` | yes | — | Set to `1` to
|
|
229
|
+
| `ADK_CODE_MODE_CONTROL_HTTP` | yes | — | Set to `1` to run the sandbox as a WebSocket server (required for remote) |
|
|
234
230
|
| `ADK_CODE_MODE_AUTH_TOKEN` | yes | — | Bearer token for WebSocket auth |
|
|
235
231
|
| `PORT` | no | `8080` | Listen port |
|
|
236
232
|
| `ADK_CODE_MODE_MAX_UPLOAD_TOOLS` | no | 100 MiB | Max tools tar archive size |
|
|
@@ -252,8 +248,6 @@ RemoteBackend(
|
|
|
252
248
|
)
|
|
253
249
|
```
|
|
254
250
|
|
|
255
|
-
`connect_timeout` defaults to `10s` — fail a stalled connect fast and retry rather than block. `RemoteBackend` retries transient connect failures up to `start_attempts` times with linear backoff + jitter; raise `connect_timeout` (and/or `start_attempts`) for platforms with slow cold starts.
|
|
256
|
-
|
|
257
251
|
## 🗂️ Storage
|
|
258
252
|
|
|
259
253
|
Code Mode exposes two file surfaces:
|
|
@@ -287,6 +281,16 @@ async def on_saved(invocation_context, delta):
|
|
|
287
281
|
CodeModeCodeExecutor(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
288
282
|
```
|
|
289
283
|
|
|
284
|
+
### Tool results as artifacts
|
|
285
|
+
|
|
286
|
+
By default (`save_tool_results_as_artifacts=True`) every non-artifact tool is wrapped so its return value is saved as a session artifact tagged `code_mode.tool_result = "true"`. This lets a host forward tool outputs to the user (read the marker in `on_artifacts_saved`) and keeps large results out of the model's context — a result whose serialised form exceeds the threshold is replaced in the reply with a short note pointing at the artifact, which the model reloads with `load_artifact`.
|
|
287
|
+
|
|
288
|
+
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
|
+
|
|
290
|
+
```python
|
|
291
|
+
CodeModeCodeExecutor(tools=..., backend=..., save_tool_results_as_artifacts=True)
|
|
292
|
+
```
|
|
293
|
+
|
|
290
294
|
## 🐳 Sandbox Image
|
|
291
295
|
|
|
292
296
|
The published base image (`ghcr.io/a2anet/adk-code-mode`) works as-is for tools whose execution is fully host-side. To bake in extra Python packages:
|
|
@@ -300,17 +304,18 @@ The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To
|
|
|
300
304
|
|
|
301
305
|
## ⚙️ Configuration
|
|
302
306
|
|
|
303
|
-
|
|
307
|
+
All settings are `CodeModeCodeExecutor` constructor arguments:
|
|
304
308
|
|
|
305
|
-
|
|
309
|
+
| Argument | Default | Purpose |
|
|
310
|
+
| --- | --- | --- |
|
|
311
|
+
| `max_catalog_chars` | `50_000` | Soft cap on the rendered tool catalog in the system prompt. When exceeded, the per-tool sections are replaced with a short note telling the model how to navigate `/tools/` from Python. |
|
|
312
|
+
| `max_output_chars` | `50_000` | Caps stdout/stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<execution-id>.txt` and the model sees a head-and-tail view pointing to it. |
|
|
313
|
+
| `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. |
|
|
316
|
+
| `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`. |
|
|
306
317
|
|
|
307
|
-
|
|
308
|
-
CodeModeCodeExecutor(tools=..., backend=..., max_catalog_chars=20_000)
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### Output truncation
|
|
312
|
-
|
|
313
|
-
`max_output_chars` (default `50_000`) caps stdout and stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<execution-id>.txt`, and the model sees a head-and-tail view with a marker pointing to it.
|
|
318
|
+
The model can read spilled stdout back from the overflow artifact:
|
|
314
319
|
|
|
315
320
|
```python
|
|
316
321
|
from tools import load_artifact
|
|
@@ -318,53 +323,9 @@ spilled = load_artifact(filename="code_mode/stdout/<execution-id>.txt")
|
|
|
318
323
|
print(spilled["data"][-2000:])
|
|
319
324
|
```
|
|
320
325
|
|
|
321
|
-
### Code size limit
|
|
322
|
-
|
|
323
|
-
`max_code_chars` (default `1_000_000`) rejects oversized code payloads before starting a container.
|
|
324
|
-
|
|
325
|
-
### Timeouts
|
|
326
|
-
|
|
327
|
-
`timeout_seconds` caps overall execution time; `per_tool_timeout_seconds` caps each individual tool call. Both default to `None` (relying on platform timeouts). Set them explicitly for defense in depth:
|
|
328
|
-
|
|
329
|
-
```python
|
|
330
|
-
CodeModeCodeExecutor(
|
|
331
|
-
tools=...,
|
|
332
|
-
backend=...,
|
|
333
|
-
timeout_seconds=30,
|
|
334
|
-
per_tool_timeout_seconds=10,
|
|
335
|
-
)
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
`timeout_seconds` / `per_tool_timeout_seconds` stay `None` by default; with turn-scoping the effective per-turn wall-clock ceiling is the platform request timeout (e.g. Cloud Run `--timeout`).
|
|
339
|
-
|
|
340
326
|
### Turn-scoped sessions
|
|
341
327
|
|
|
342
|
-
A sandbox container is held open for
|
|
343
|
-
|
|
344
|
-
The container is released when the turn ends. Wire `code_executor.release_invocation(callback_context.invocation_id)` into your agent's `after_agent_callback` to free it the moment the turn finishes:
|
|
345
|
-
|
|
346
|
-
```python
|
|
347
|
-
def _release(callback_context):
|
|
348
|
-
code_executor.release_invocation(callback_context.invocation_id)
|
|
349
|
-
|
|
350
|
-
agent = LlmAgent(..., after_agent_callback=[_release])
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Two safety nets cover turns that never signal a clean end: an idle reaper closes sessions idle longer than `session_idle_timeout_seconds` (default `600`), and a mid-turn connection loss (or a `timeout_seconds` firing) drops the session — in-turn state (globals + `/workspace`) is lost and the next block reconnects on a fresh container.
|
|
354
|
-
|
|
355
|
-
If the connection drops **while a block is running**, the executor re-runs the block only when the code provably never reached the sandbox, so a reconnect can't duplicate a tool call's side effects. What happens depends on how far the block got before the drop:
|
|
356
|
-
|
|
357
|
-
- **Not run** — the `RunFrame` never left the host, so the code did not execute; the executor reconnects on a fresh container and retries the block once automatically.
|
|
358
|
-
- **Ran (output lost)** — a `DoneFrame` came back before the drop, so the code executed but its stdout/stderr and output files couldn't be returned; the block is **not** re-run.
|
|
359
|
-
- **Unknown** — the `RunFrame` was sent but no `DoneFrame` arrived, so whether the code executed is unknowable; the block is **not** re-run.
|
|
360
|
-
|
|
361
|
-
For the `Ran` and `Unknown` cases the block returns a short message in its `stderr` telling the model what happened, so it can decide whether to re-run the code or check for its effects rather than the executor silently repeating it.
|
|
362
|
-
|
|
363
|
-
```python
|
|
364
|
-
CodeModeCodeExecutor(tools=..., backend=..., session_idle_timeout_seconds=600)
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
> Turn-scoping is wire-protocol **v2**. The host wheel and the sandbox image must be upgraded together — a version mismatch fails fast with `ProtocolVersionMismatchError` — so rebuild and push the sandbox image when you upgrade.
|
|
328
|
+
A sandbox container is held open for one **turn** (one ADK invocation) and reused across that turn's code blocks, so cold start is paid at most once per turn. Python globals **and** `/workspace` persist across a turn's blocks and reset between turns — use ADK Artifacts for cross-turn persistence. The container is released when the turn ends (via `release_invocation`, wired in [Usage](#-usage)) and destroyed by the platform, so no state survives into another turn or tenant.
|
|
368
329
|
|
|
369
330
|
## 🏗️ Architecture
|
|
370
331
|
|
|
@@ -383,37 +344,40 @@ The only things crossing the boundary are: code, tool call arguments, tool retur
|
|
|
383
344
|
|
|
384
345
|
Your `instruction` (containing `CODE_MODE_SYSTEM_INSTRUCTION`) followed by a `<code-mode>` block appended by the callback:
|
|
385
346
|
|
|
386
|
-
|
|
347
|
+
~~~
|
|
387
348
|
…your instruction…
|
|
388
349
|
|
|
389
350
|
# How to execute code and use tools
|
|
390
|
-
|
|
391
|
-
The Python Standard Library and a
|
|
392
|
-
To see the result of your code, you need to print it.
|
|
393
|
-
|
|
394
|
-
For example, if you had the following tool:
|
|
351
|
+
You have no callable function tools. To use a tool you must write Python code in a fenced Python block (i.e. ```python\n...\n```). Do not emit a function or tool call.
|
|
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:
|
|
395
354
|
|
|
396
355
|
```
|
|
356
|
+
<code-mode>
|
|
357
|
+
|
|
397
358
|
from tools.slack import send_message
|
|
398
359
|
|
|
399
360
|
def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
|
|
400
361
|
"""Send a message to a Slack channel."""
|
|
401
362
|
...
|
|
363
|
+
|
|
364
|
+
</code-mode>
|
|
402
365
|
```
|
|
403
366
|
|
|
404
367
|
To call the tool, you should write:
|
|
405
368
|
|
|
406
|
-
|
|
369
|
+
````
|
|
407
370
|
```python
|
|
408
371
|
from tools.slack import send_message
|
|
409
372
|
|
|
410
373
|
print(send_message(channel="C123", text="hi"))
|
|
411
374
|
```
|
|
412
|
-
|
|
375
|
+
````
|
|
413
376
|
|
|
414
377
|
# How to use files and variables in between executions
|
|
415
|
-
|
|
416
|
-
To
|
|
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.
|
|
417
381
|
|
|
418
382
|
<code-mode>
|
|
419
383
|
|
|
@@ -435,7 +399,9 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
435
399
|
…
|
|
436
400
|
|
|
437
401
|
</code-mode>
|
|
438
|
-
|
|
402
|
+
~~~
|
|
403
|
+
|
|
404
|
+
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.
|
|
439
405
|
|
|
440
406
|
When the rendered catalog exceeds `max_catalog_chars`, the per-tool sections are replaced with:
|
|
441
407
|
|
|
@@ -14,6 +14,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
14
14
|
- **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.
|
|
15
15
|
- **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.
|
|
16
16
|
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
17
|
+
- **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`.
|
|
17
18
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
18
19
|
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's code blocks. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
|
|
19
20
|
- **Local development** — `UnsafeLocalDockerBackend` runs the sandbox against your local Docker daemon for fast iteration. **Not for production** — see [Safety](#-safety).
|
|
@@ -51,11 +52,12 @@ Requires Python 3.10+. Local development requires [Docker](https://docs.docker.c
|
|
|
51
52
|
|
|
52
53
|
## 🚀 Usage
|
|
53
54
|
|
|
54
|
-
Build a `CodeModeCodeExecutor`, then wire
|
|
55
|
+
Build a `CodeModeCodeExecutor`, then wire four things into the agent:
|
|
55
56
|
|
|
56
57
|
- **`CODE_MODE_SYSTEM_INSTRUCTION`** — append to the agent's `instruction`. Teaches the model how to write code blocks and use artifacts.
|
|
57
58
|
- **`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.
|
|
58
59
|
- **`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=[]`).
|
|
60
|
+
- **`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.
|
|
59
61
|
|
|
60
62
|
### Production (remote sandbox)
|
|
61
63
|
|
|
@@ -77,6 +79,9 @@ executor = CodeModeCodeExecutor(
|
|
|
77
79
|
),
|
|
78
80
|
)
|
|
79
81
|
|
|
82
|
+
def _release_sandbox(callback_context):
|
|
83
|
+
executor.release_invocation(callback_context.invocation_id)
|
|
84
|
+
|
|
80
85
|
root_agent = LlmAgent(
|
|
81
86
|
name="assistant",
|
|
82
87
|
model="gemini-2.5-pro",
|
|
@@ -89,6 +94,7 @@ root_agent = LlmAgent(
|
|
|
89
94
|
),
|
|
90
95
|
),
|
|
91
96
|
before_model_callback=code_mode_before_model_callback(executor),
|
|
97
|
+
after_agent_callback=[_release_sandbox],
|
|
92
98
|
)
|
|
93
99
|
```
|
|
94
100
|
|
|
@@ -119,16 +125,7 @@ print(send_message(channel="C123", text="hi"))
|
|
|
119
125
|
|
|
120
126
|
## 🌐 Remote Deployment
|
|
121
127
|
|
|
122
|
-
**Every turn runs in its own container
|
|
123
|
-
|
|
124
|
-
Setting `ADK_CODE_MODE_CONTROL_HTTP=1` activates HTTP mode. The container:
|
|
125
|
-
|
|
126
|
-
1. Starts a WebSocket server on port 8080 (configurable via `PORT`)
|
|
127
|
-
2. Accepts exactly one connection (rejects further connections with 503)
|
|
128
|
-
3. Receives the tools tar **once**, then announces readiness
|
|
129
|
-
4. Sanitizes the environment (strips all env vars except a safe allowlist)
|
|
130
|
-
5. For each code block: receives that block's workspace tar, executes user code (tools proxied back to the host over the same WebSocket; globals + `/workspace` persist across blocks), then returns stdout/stderr and the updated workspace
|
|
131
|
-
6. Exits when the turn ends (`ShutdownFrame` or disconnect)
|
|
128
|
+
**Every turn runs in its own container**, which the platform destroys when the turn ends — no cross-turn or cross-tenant state. The sandbox runs as a WebSocket server (set `ADK_CODE_MODE_CONTROL_HTTP=1`) and accepts exactly one connection, so you **must** configure your platform for one container per turn (`--concurrency 1` on Cloud Run, or equivalent).
|
|
132
129
|
|
|
133
130
|
### Deploy to Cloud Run
|
|
134
131
|
|
|
@@ -156,7 +153,7 @@ gcloud compute networks vpc-access connectors create adk-sandbox-connector \
|
|
|
156
153
|
--region=<region> \
|
|
157
154
|
--subnet=adk-sandbox-subnet
|
|
158
155
|
|
|
159
|
-
# Deploy — note --concurrency 1
|
|
156
|
+
# Deploy — note --concurrency 1, --vpc-egress=all-traffic, and the /health startup probe
|
|
160
157
|
gcloud run deploy adk-code-mode-sandbox \
|
|
161
158
|
--image <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest \
|
|
162
159
|
--region <region> \
|
|
@@ -170,10 +167,11 @@ gcloud run deploy adk-code-mode-sandbox \
|
|
|
170
167
|
--vpc-connector=adk-sandbox-connector \
|
|
171
168
|
--vpc-egress=all-traffic \
|
|
172
169
|
--set-env-vars "ADK_CODE_MODE_CONTROL_HTTP=1" \
|
|
173
|
-
--set-secrets "ADK_CODE_MODE_AUTH_TOKEN=<your-secret-name>:latest"
|
|
170
|
+
--set-secrets "ADK_CODE_MODE_AUTH_TOKEN=<your-secret-name>:latest" \
|
|
171
|
+
--startup-probe "httpGet.path=/health,httpGet.port=8080,timeoutSeconds=3,periodSeconds=3,failureThreshold=80"
|
|
174
172
|
```
|
|
175
173
|
|
|
176
|
-
> These flags are **recommendations to tune per deployment**, not hard requirements. `--timeout 3600` (Cloud Run's max) is the per-turn ceiling since the container holds the WebSocket for the whole turn; `--max-instances` should cover your peak concurrent *turns* (`120` covers a 10–100 target — verify your region's Cloud Run vCPU quota).
|
|
174
|
+
> These flags are **recommendations to tune per deployment**, not hard requirements. `--timeout 3600` (Cloud Run's max) is the per-turn ceiling since the container holds the WebSocket for the whole turn; `--max-instances` should cover your peak concurrent *turns* (`120` covers a 10–100 target — verify your region's Cloud Run vCPU quota). The `/health` startup probe avoids cold-start `HTTP 503`s — Cloud Run's default TCP probe opens a raw socket the WebSocket server rejects.
|
|
177
175
|
|
|
178
176
|
Then in your agent:
|
|
179
177
|
|
|
@@ -188,22 +186,20 @@ RemoteBackend(
|
|
|
188
186
|
|
|
189
187
|
> **`--vpc-egress=all-traffic` with a deny-all VPC is critical for security.** Without it, user code can make arbitrary outbound requests — including hitting the GCP metadata endpoint (`169.254.169.254`) to steal the service account token, exfiltrating data, or scanning your VPC. The sandbox only needs to _accept_ inbound connections; it never needs outbound access.
|
|
190
188
|
|
|
191
|
-
> **Configure an HTTP startup probe against `/health`.** Cloud Run's default TCP probe opens a raw socket that the `websockets` server rejects as a malformed HTTP request, which can race with real traffic during cold starts and surface to clients as `HTTP 503` on the WebSocket handshake. Add `--startup-probe="httpGet.path=/health,httpGet.port=8080,timeoutSeconds=3,periodSeconds=3,failureThreshold=80"` (or equivalent) so probes hit the `/health` endpoint the sandbox handles natively. Keep `periodSeconds` small (e.g. `3`): it sets how long Cloud Run waits before re-probing, so a short period lets a ready container start serving in ~3s instead of ~10s — raise `failureThreshold` to keep the same overall startup budget (`periodSeconds × failureThreshold`). Turn-scoping already gives demand-driven warmth (a container stays up for the whole turn), so `--min-instances=0` is fine; only set `--min-instances` if you want to eliminate cold starts at the cost of a paid warm pool.
|
|
192
|
-
|
|
193
189
|
### Deploy on other platforms
|
|
194
190
|
|
|
195
191
|
The same pattern works on any platform that runs Docker containers as HTTP services (AWS Fargate/ECS, Azure Container Instances, Kubernetes, Fly.io, etc.):
|
|
196
192
|
|
|
197
193
|
1. **One container per turn.** Each container handles exactly one turn (one or more code blocks) and exits.
|
|
198
194
|
2. **Block all outbound network access.** Without egress restrictions, user code can exfiltrate data, access cloud metadata endpoints, or scan internal networks.
|
|
199
|
-
3. **
|
|
195
|
+
3. **Keep `/workspace` and `/tools` writable.** The sandbox stages input files into `/workspace` 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`).
|
|
200
196
|
4. **Authenticate connections.** Set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth (IAM, NetworkPolicy, security groups) on top.
|
|
201
197
|
|
|
202
198
|
Required env vars:
|
|
203
199
|
|
|
204
200
|
| Env var | Required | Default | Purpose |
|
|
205
201
|
| ------------------------------------ | -------- | ------- | -------------------------------- |
|
|
206
|
-
| `ADK_CODE_MODE_CONTROL_HTTP` | yes | — | Set to `1` to
|
|
202
|
+
| `ADK_CODE_MODE_CONTROL_HTTP` | yes | — | Set to `1` to run the sandbox as a WebSocket server (required for remote) |
|
|
207
203
|
| `ADK_CODE_MODE_AUTH_TOKEN` | yes | — | Bearer token for WebSocket auth |
|
|
208
204
|
| `PORT` | no | `8080` | Listen port |
|
|
209
205
|
| `ADK_CODE_MODE_MAX_UPLOAD_TOOLS` | no | 100 MiB | Max tools tar archive size |
|
|
@@ -225,8 +221,6 @@ RemoteBackend(
|
|
|
225
221
|
)
|
|
226
222
|
```
|
|
227
223
|
|
|
228
|
-
`connect_timeout` defaults to `10s` — fail a stalled connect fast and retry rather than block. `RemoteBackend` retries transient connect failures up to `start_attempts` times with linear backoff + jitter; raise `connect_timeout` (and/or `start_attempts`) for platforms with slow cold starts.
|
|
229
|
-
|
|
230
224
|
## 🗂️ Storage
|
|
231
225
|
|
|
232
226
|
Code Mode exposes two file surfaces:
|
|
@@ -260,6 +254,16 @@ async def on_saved(invocation_context, delta):
|
|
|
260
254
|
CodeModeCodeExecutor(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
261
255
|
```
|
|
262
256
|
|
|
257
|
+
### Tool results as artifacts
|
|
258
|
+
|
|
259
|
+
By default (`save_tool_results_as_artifacts=True`) every non-artifact tool is wrapped so its return value is saved as a session artifact tagged `code_mode.tool_result = "true"`. This lets a host forward tool outputs to the user (read the marker in `on_artifacts_saved`) and keeps large results out of the model's context — a result whose serialised form exceeds the threshold is replaced in the reply with a short note pointing at the artifact, which the model reloads with `load_artifact`.
|
|
260
|
+
|
|
261
|
+
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.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
CodeModeCodeExecutor(tools=..., backend=..., save_tool_results_as_artifacts=True)
|
|
265
|
+
```
|
|
266
|
+
|
|
263
267
|
## 🐳 Sandbox Image
|
|
264
268
|
|
|
265
269
|
The published base image (`ghcr.io/a2anet/adk-code-mode`) works as-is for tools whose execution is fully host-side. To bake in extra Python packages:
|
|
@@ -273,17 +277,18 @@ The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To
|
|
|
273
277
|
|
|
274
278
|
## ⚙️ Configuration
|
|
275
279
|
|
|
276
|
-
|
|
280
|
+
All settings are `CodeModeCodeExecutor` constructor arguments:
|
|
277
281
|
|
|
278
|
-
|
|
282
|
+
| Argument | Default | Purpose |
|
|
283
|
+
| --- | --- | --- |
|
|
284
|
+
| `max_catalog_chars` | `50_000` | Soft cap on the rendered tool catalog in the system prompt. When exceeded, the per-tool sections are replaced with a short note telling the model how to navigate `/tools/` from Python. |
|
|
285
|
+
| `max_output_chars` | `50_000` | Caps stdout/stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<execution-id>.txt` and the model sees a head-and-tail view pointing to it. |
|
|
286
|
+
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
287
|
+
| `timeout_seconds` | `None` | Caps overall execution time. Defaults to the platform request timeout (e.g. Cloud Run `--timeout`); set explicitly for defense in depth. |
|
|
288
|
+
| `per_tool_timeout_seconds` | `None` | Caps each individual tool call. |
|
|
289
|
+
| `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`. |
|
|
279
290
|
|
|
280
|
-
|
|
281
|
-
CodeModeCodeExecutor(tools=..., backend=..., max_catalog_chars=20_000)
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Output truncation
|
|
285
|
-
|
|
286
|
-
`max_output_chars` (default `50_000`) caps stdout and stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<execution-id>.txt`, and the model sees a head-and-tail view with a marker pointing to it.
|
|
291
|
+
The model can read spilled stdout back from the overflow artifact:
|
|
287
292
|
|
|
288
293
|
```python
|
|
289
294
|
from tools import load_artifact
|
|
@@ -291,53 +296,9 @@ spilled = load_artifact(filename="code_mode/stdout/<execution-id>.txt")
|
|
|
291
296
|
print(spilled["data"][-2000:])
|
|
292
297
|
```
|
|
293
298
|
|
|
294
|
-
### Code size limit
|
|
295
|
-
|
|
296
|
-
`max_code_chars` (default `1_000_000`) rejects oversized code payloads before starting a container.
|
|
297
|
-
|
|
298
|
-
### Timeouts
|
|
299
|
-
|
|
300
|
-
`timeout_seconds` caps overall execution time; `per_tool_timeout_seconds` caps each individual tool call. Both default to `None` (relying on platform timeouts). Set them explicitly for defense in depth:
|
|
301
|
-
|
|
302
|
-
```python
|
|
303
|
-
CodeModeCodeExecutor(
|
|
304
|
-
tools=...,
|
|
305
|
-
backend=...,
|
|
306
|
-
timeout_seconds=30,
|
|
307
|
-
per_tool_timeout_seconds=10,
|
|
308
|
-
)
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
`timeout_seconds` / `per_tool_timeout_seconds` stay `None` by default; with turn-scoping the effective per-turn wall-clock ceiling is the platform request timeout (e.g. Cloud Run `--timeout`).
|
|
312
|
-
|
|
313
299
|
### Turn-scoped sessions
|
|
314
300
|
|
|
315
|
-
A sandbox container is held open for
|
|
316
|
-
|
|
317
|
-
The container is released when the turn ends. Wire `code_executor.release_invocation(callback_context.invocation_id)` into your agent's `after_agent_callback` to free it the moment the turn finishes:
|
|
318
|
-
|
|
319
|
-
```python
|
|
320
|
-
def _release(callback_context):
|
|
321
|
-
code_executor.release_invocation(callback_context.invocation_id)
|
|
322
|
-
|
|
323
|
-
agent = LlmAgent(..., after_agent_callback=[_release])
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Two safety nets cover turns that never signal a clean end: an idle reaper closes sessions idle longer than `session_idle_timeout_seconds` (default `600`), and a mid-turn connection loss (or a `timeout_seconds` firing) drops the session — in-turn state (globals + `/workspace`) is lost and the next block reconnects on a fresh container.
|
|
327
|
-
|
|
328
|
-
If the connection drops **while a block is running**, the executor re-runs the block only when the code provably never reached the sandbox, so a reconnect can't duplicate a tool call's side effects. What happens depends on how far the block got before the drop:
|
|
329
|
-
|
|
330
|
-
- **Not run** — the `RunFrame` never left the host, so the code did not execute; the executor reconnects on a fresh container and retries the block once automatically.
|
|
331
|
-
- **Ran (output lost)** — a `DoneFrame` came back before the drop, so the code executed but its stdout/stderr and output files couldn't be returned; the block is **not** re-run.
|
|
332
|
-
- **Unknown** — the `RunFrame` was sent but no `DoneFrame` arrived, so whether the code executed is unknowable; the block is **not** re-run.
|
|
333
|
-
|
|
334
|
-
For the `Ran` and `Unknown` cases the block returns a short message in its `stderr` telling the model what happened, so it can decide whether to re-run the code or check for its effects rather than the executor silently repeating it.
|
|
335
|
-
|
|
336
|
-
```python
|
|
337
|
-
CodeModeCodeExecutor(tools=..., backend=..., session_idle_timeout_seconds=600)
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
> Turn-scoping is wire-protocol **v2**. The host wheel and the sandbox image must be upgraded together — a version mismatch fails fast with `ProtocolVersionMismatchError` — so rebuild and push the sandbox image when you upgrade.
|
|
301
|
+
A sandbox container is held open for one **turn** (one ADK invocation) and reused across that turn's code blocks, so cold start is paid at most once per turn. Python globals **and** `/workspace` persist across a turn's blocks and reset between turns — use ADK Artifacts for cross-turn persistence. The container is released when the turn ends (via `release_invocation`, wired in [Usage](#-usage)) and destroyed by the platform, so no state survives into another turn or tenant.
|
|
341
302
|
|
|
342
303
|
## 🏗️ Architecture
|
|
343
304
|
|
|
@@ -356,37 +317,40 @@ The only things crossing the boundary are: code, tool call arguments, tool retur
|
|
|
356
317
|
|
|
357
318
|
Your `instruction` (containing `CODE_MODE_SYSTEM_INSTRUCTION`) followed by a `<code-mode>` block appended by the callback:
|
|
358
319
|
|
|
359
|
-
|
|
320
|
+
~~~
|
|
360
321
|
…your instruction…
|
|
361
322
|
|
|
362
323
|
# How to execute code and use tools
|
|
363
|
-
|
|
364
|
-
The Python Standard Library and a
|
|
365
|
-
To see the result of your code, you need to print it.
|
|
366
|
-
|
|
367
|
-
For example, if you had the following tool:
|
|
324
|
+
You have no callable function tools. To use a tool you must write Python code in a fenced Python block (i.e. ```python\n...\n```). Do not emit a function or tool call.
|
|
325
|
+
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.
|
|
326
|
+
To see the result of your code, you need to print it. For example, if you had the following tool:
|
|
368
327
|
|
|
369
328
|
```
|
|
329
|
+
<code-mode>
|
|
330
|
+
|
|
370
331
|
from tools.slack import send_message
|
|
371
332
|
|
|
372
333
|
def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
|
|
373
334
|
"""Send a message to a Slack channel."""
|
|
374
335
|
...
|
|
336
|
+
|
|
337
|
+
</code-mode>
|
|
375
338
|
```
|
|
376
339
|
|
|
377
340
|
To call the tool, you should write:
|
|
378
341
|
|
|
379
|
-
|
|
342
|
+
````
|
|
380
343
|
```python
|
|
381
344
|
from tools.slack import send_message
|
|
382
345
|
|
|
383
346
|
print(send_message(channel="C123", text="hi"))
|
|
384
347
|
```
|
|
385
|
-
|
|
348
|
+
````
|
|
386
349
|
|
|
387
350
|
# How to use files and variables in between executions
|
|
388
|
-
|
|
389
|
-
To
|
|
351
|
+
You can reuse variables and files within a turn. Between turns, to reuse variables and files you must save and load Artifacts.
|
|
352
|
+
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.
|
|
353
|
+
Tool results are automatically saved as Artifacts.
|
|
390
354
|
|
|
391
355
|
<code-mode>
|
|
392
356
|
|
|
@@ -408,7 +372,9 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
408
372
|
…
|
|
409
373
|
|
|
410
374
|
</code-mode>
|
|
411
|
-
|
|
375
|
+
~~~
|
|
376
|
+
|
|
377
|
+
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.
|
|
412
378
|
|
|
413
379
|
When the rendered catalog exceeds `max_catalog_chars`, the per-tool sections are replaced with:
|
|
414
380
|
|
|
@@ -9,8 +9,8 @@ RUN pip install --no-cache-dir /tmp/adk_code_mode_sandbox-*.whl \
|
|
|
9
9
|
&& rm /tmp/adk_code_mode_sandbox-*.whl
|
|
10
10
|
|
|
11
11
|
RUN useradd -m -d /home/sandbox sandbox \
|
|
12
|
-
&& mkdir -p /workspace \
|
|
13
|
-
&& chown sandbox:sandbox /workspace
|
|
12
|
+
&& mkdir -p /workspace /tools \
|
|
13
|
+
&& chown sandbox:sandbox /workspace /tools
|
|
14
14
|
USER sandbox
|
|
15
15
|
|
|
16
16
|
EXPOSE 8080
|