adk-code-mode 0.2.2__tar.gz → 0.4.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-0.4.0/.release-please-manifest.json +3 -0
- adk_code_mode-0.4.0/CHANGELOG.md +77 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/PKG-INFO +68 -60
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/README.md +67 -59
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/docker/Dockerfile +2 -2
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +38 -9
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +109 -74
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +6 -2
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/__about__.py +1 -1
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/__init__.py +14 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/callback.py +1 -1
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/executor.py +449 -92
- adk_code_mode-0.4.0/src/adk_code_mode/runtime/base.py +126 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/docker.py +56 -33
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/protocol.py +6 -2
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/remote.py +116 -25
- adk_code_mode-0.4.0/src/adk_code_mode/tool_result_artifacts.py +187 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/_fake_runtime.py +42 -12
- adk_code_mode-0.4.0/tests/conftest.py +37 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_docker_integration.py +43 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_host_loop.py +4 -1
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_integration.py +345 -1
- adk_code_mode-0.4.0/tests/test_remote_backend.py +82 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_remote_integration.py +149 -1
- adk_code_mode-0.4.0/tests/test_tool_result_artifacts.py +177 -0
- adk_code_mode-0.2.2/.release-please-manifest.json +0 -3
- adk_code_mode-0.2.2/CHANGELOG.md +0 -44
- adk_code_mode-0.2.2/src/adk_code_mode/runtime/base.py +0 -88
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.githooks/pre-commit +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.githooks/pre-push +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.gitignore +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.prettierrc +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.vscode/settings.json +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.zed/settings.json +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/AGENTS.md +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/CLAUDE.md +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/LICENSE +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/Makefile +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/pyproject.toml +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/release-please-config.json +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/_artifact_tools.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/output.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/catalog.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/namespacing.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/normaliser.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/stubs.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/workspace/__init__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/workspace/files.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/__init__.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_agent_with_fake_llm.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_artifact_tools.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_callback.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_catalog.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_dispatcher.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_docker_image.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_namespacing.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_normaliser.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_output.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_protocol.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_stubs.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_version.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_workspace_files.py +0 -0
- {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [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)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* save tool results as artifacts with optional model-supplied naming ([76a3671](https://github.com/a2anet/adk-code-mode/commit/76a36713b2c52bd5b8dc51d3669226a47e7108e5))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* align code-mode example with the `<code-mode>` catalog and fix fences ([4a0cc6d](https://github.com/a2anet/adk-code-mode/commit/4a0cc6d0fc99d5425f6682cc489e76c9ddef7d46))
|
|
14
|
+
* clarify code-mode tools are a Python library, not function tools ([48d1018](https://github.com/a2anet/adk-code-mode/commit/48d1018152a2ed2a8e7361de6ce0f43b31b429ce))
|
|
15
|
+
* nudge the model to call tools via Python in the code-mode block ([141baf3](https://github.com/a2anet/adk-code-mode/commit/141baf3a44976e82cf437b2064666be893d6d409))
|
|
16
|
+
* update `CODE_MODE_SYSTEM_INSTRUCTION` and `README.md` ([933d42c](https://github.com/a2anet/adk-code-mode/commit/933d42cb237b9d6deeb6611511cba28a0d5e9261))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* streamline `README.md` and fix broken code fences ([c237514](https://github.com/a2anet/adk-code-mode/commit/c2375149df48e21318967090b6c85d0b1f411ea2))
|
|
22
|
+
|
|
23
|
+
## [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)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* hold one sandbox container open per turn (protocol v2) ([#6](https://github.com/a2anet/adk-code-mode/issues/6)) ([aebe17d](https://github.com/a2anet/adk-code-mode/commit/aebe17d2bbbf21cc0f05852efaf910ff6adc3154))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Documentation
|
|
32
|
+
|
|
33
|
+
* lower startup probe period in `README.md` for faster cold starts ([e251af2](https://github.com/a2anet/adk-code-mode/commit/e251af2cf7307c9944361e1a0e37c7191822a9e1))
|
|
34
|
+
* note HTTP `/health` startup probe in `README.md` Cloud Run section ([d58fb10](https://github.com/a2anet/adk-code-mode/commit/d58fb10f7aeda95e5c8cf29263a13e1333f661dd))
|
|
35
|
+
|
|
36
|
+
## [0.2.2](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.2.1...adk-code-mode-v0.2.2) (2026-05-08)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
* use `python` fences instead of `tool_code` in `CodeModeCodeExecutor` ([efcc11e](https://github.com/a2anet/adk-code-mode/commit/efcc11e1020c7544223074e00a8649f9c85d4ad6))
|
|
42
|
+
|
|
43
|
+
## [0.2.1](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.2.0...adk-code-mode-v0.2.1) (2026-05-05)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* rename `<tools>` tag to `<code-mode>` and update `CODE_MODE_SYSTEM_INSTRUCTION` ([0a06bfb](https://github.com/a2anet/adk-code-mode/commit/0a06bfbdf22db6e0226e477d0ed074a6cc4d1515))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
* update `README.md` with deployment instructions ([1dd7a95](https://github.com/a2anet/adk-code-mode/commit/1dd7a95197fd56231ba97182526a825baf18508a))
|
|
54
|
+
|
|
55
|
+
## [0.2.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.1.0...adk-code-mode-v0.2.0) (2026-05-03)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### Features
|
|
59
|
+
|
|
60
|
+
* add `RemoteBackend`, sandbox hardening, and make `README.md` more concise ([a1a70ee](https://github.com/a2anet/adk-code-mode/commit/a1a70eec0686f164ce3796ba1d82bb82b7fbda59))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
* improve `CODE_MODE_SYSTEM_INSTRUCTION` ([0d00863](https://github.com/a2anet/adk-code-mode/commit/0d008636e2e273b438192f737e20e207c165a610))
|
|
66
|
+
|
|
67
|
+
## 0.1.0 (2026-04-29)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### Features
|
|
71
|
+
|
|
72
|
+
* ADK Code Mode: `CodeModeExecutor`, `DockerRuntime`, and more ([6ab1ac2](https://github.com/a2anet/adk-code-mode/commit/6ab1ac28236d21ecb740d8e920e08e4d9bd969c5))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Documentation
|
|
76
|
+
|
|
77
|
+
* shorten ADK code executor comparison table headers ([28116cf](https://github.com/a2anet/adk-code-mode/commit/28116cf9f40b3623c31782a55885fa5c4f8c6bbf))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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,15 +41,16 @@ 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
|
-
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated
|
|
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).
|
|
47
48
|
|
|
48
49
|
| | BuiltIn | AgentEngineSandbox | VertexAi | Container | Gke | CodeMode |
|
|
49
50
|
| ----------------------------------- | ------- | ------------------------------- | ------------------------------- | --------- | --- | ------------------------ |
|
|
50
51
|
| Call ADK tools from code | no | no | no | no | no | yes (with limitations) |
|
|
51
52
|
| Extra Python packages | no | no (more than stdlib but fixed) | no (more than stdlib but fixed) | yes | yes | yes |
|
|
52
|
-
| Variables are stateful | no | yes | yes | no | no |
|
|
53
|
+
| Variables are stateful | no | yes | yes | no | no | yes (within a turn) |
|
|
53
54
|
| Input files | no | yes | yes | no | no | yes |
|
|
54
55
|
| Output files | no | yes | yes | no | no | yes |
|
|
55
56
|
| Storage | no | yes (via variables) | yes (via variables) | no | no | yes (via ADK Artifacts) |
|
|
@@ -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,17 +152,7 @@ print(send_message(channel="C123", text="hi"))
|
|
|
146
152
|
|
|
147
153
|
## 🌐 Remote Deployment
|
|
148
154
|
|
|
149
|
-
**Every
|
|
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 tools and workspace as tar archives over binary WebSocket frames
|
|
156
|
-
4. Sanitizes the environment (strips all env vars except a safe allowlist)
|
|
157
|
-
5. Executes user code with tools proxied back to the host over the same WebSocket
|
|
158
|
-
6. Returns stdout/stderr and updated workspace files
|
|
159
|
-
7. Exits
|
|
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).
|
|
160
156
|
|
|
161
157
|
### Deploy to Cloud Run
|
|
162
158
|
|
|
@@ -184,7 +180,7 @@ gcloud compute networks vpc-access connectors create adk-sandbox-connector \
|
|
|
184
180
|
--region=<region> \
|
|
185
181
|
--subnet=adk-sandbox-subnet
|
|
186
182
|
|
|
187
|
-
# Deploy — note --concurrency 1
|
|
183
|
+
# Deploy — note --concurrency 1, --vpc-egress=all-traffic, and the /health startup probe
|
|
188
184
|
gcloud run deploy adk-code-mode-sandbox \
|
|
189
185
|
--image <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest \
|
|
190
186
|
--region <region> \
|
|
@@ -192,13 +188,18 @@ gcloud run deploy adk-code-mode-sandbox \
|
|
|
192
188
|
--cpu 1 \
|
|
193
189
|
--memory 1Gi \
|
|
194
190
|
--concurrency 1 \
|
|
191
|
+
--timeout 3600 \
|
|
192
|
+
--max-instances 120 \
|
|
195
193
|
--allow-unauthenticated \
|
|
196
194
|
--vpc-connector=adk-sandbox-connector \
|
|
197
195
|
--vpc-egress=all-traffic \
|
|
198
196
|
--set-env-vars "ADK_CODE_MODE_CONTROL_HTTP=1" \
|
|
199
|
-
--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"
|
|
200
199
|
```
|
|
201
200
|
|
|
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.
|
|
202
|
+
|
|
202
203
|
Then in your agent:
|
|
203
204
|
|
|
204
205
|
```python
|
|
@@ -208,7 +209,7 @@ RemoteBackend(
|
|
|
208
209
|
)
|
|
209
210
|
```
|
|
210
211
|
|
|
211
|
-
> **`--concurrency 1` is critical for security.** Without this flag, Cloud Run may route multiple
|
|
212
|
+
> **`--concurrency 1` is critical for security.** It pins one turn to one container. Without this flag, Cloud Run may route multiple turns to the same container. The sandbox rejects the second connection, but the misconfiguration itself is a risk.
|
|
212
213
|
|
|
213
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.
|
|
214
215
|
|
|
@@ -216,7 +217,7 @@ RemoteBackend(
|
|
|
216
217
|
|
|
217
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.):
|
|
218
219
|
|
|
219
|
-
1. **One container per
|
|
220
|
+
1. **One container per turn.** Each container handles exactly one turn (one or more code blocks) and exits.
|
|
220
221
|
2. **Block all outbound network access.** Without egress restrictions, user code can exfiltrate data, access cloud metadata endpoints, or scan internal networks.
|
|
221
222
|
3. **Set a read-only root filesystem** where the platform supports it (e.g., `readOnlyRootFilesystem: true` in Kubernetes). The sandbox only writes to `/workspace`.
|
|
222
223
|
4. **Authenticate connections.** Set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth (IAM, NetworkPolicy, security groups) on top.
|
|
@@ -225,18 +226,22 @@ Required env vars:
|
|
|
225
226
|
|
|
226
227
|
| Env var | Required | Default | Purpose |
|
|
227
228
|
| ------------------------------------ | -------- | ------- | -------------------------------- |
|
|
228
|
-
| `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) |
|
|
229
230
|
| `ADK_CODE_MODE_AUTH_TOKEN` | yes | — | Bearer token for WebSocket auth |
|
|
230
231
|
| `PORT` | no | `8080` | Listen port |
|
|
231
232
|
| `ADK_CODE_MODE_MAX_UPLOAD_TOOLS` | no | 100 MiB | Max tools tar archive size |
|
|
232
233
|
| `ADK_CODE_MODE_MAX_UPLOAD_WORKSPACE` | no | 100 MiB | Max workspace tar archive size |
|
|
233
234
|
|
|
234
|
-
|
|
235
|
+
Connection tuning, retry, and the same upload limits (plus a download limit) are configurable on `RemoteBackend`:
|
|
235
236
|
|
|
236
237
|
```python
|
|
237
238
|
RemoteBackend(
|
|
238
239
|
url="...",
|
|
239
240
|
token="...",
|
|
241
|
+
connect_timeout=10.0, # seconds to wait for the WS handshake (default)
|
|
242
|
+
start_attempts=3, # connect attempts before giving up (default)
|
|
243
|
+
start_retry_delay_seconds=1.0, # linear backoff base: delay * attempt (default)
|
|
244
|
+
start_retry_jitter_seconds=0.25, # uniform jitter added per retry (default)
|
|
240
245
|
max_upload_tools_bytes=100 * 1024 * 1024, # 100 MiB (default)
|
|
241
246
|
max_upload_workspace_bytes=100 * 1024 * 1024, # 100 MiB (default)
|
|
242
247
|
max_download_workspace_bytes=100 * 1024 * 1024, # 100 MiB (default)
|
|
@@ -247,7 +252,7 @@ RemoteBackend(
|
|
|
247
252
|
|
|
248
253
|
Code Mode exposes two file surfaces:
|
|
249
254
|
|
|
250
|
-
- **`/workspace`** —
|
|
255
|
+
- **`/workspace`** — the turn's working directory. It persists across the turn's code blocks and resets between turns. ADK `input_files` for a block are staged here before that block runs (`open("input.csv")` works). Files whose content changed in a block are returned as that block's `CodeExecutionResult.output_files`; nothing under `/workspace` is re-hydrated on the next turn unless persisted via `save_artifact`.
|
|
251
256
|
|
|
252
257
|
- **ADK Artifacts** — persistent cross-turn storage. `CodeModeCodeExecutor` injects three tools into the catalog:
|
|
253
258
|
|
|
@@ -276,6 +281,16 @@ async def on_saved(invocation_context, delta):
|
|
|
276
281
|
CodeModeCodeExecutor(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
277
282
|
```
|
|
278
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
|
+
|
|
279
294
|
## 🐳 Sandbox Image
|
|
280
295
|
|
|
281
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:
|
|
@@ -289,17 +304,18 @@ The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To
|
|
|
289
304
|
|
|
290
305
|
## ⚙️ Configuration
|
|
291
306
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`max_catalog_chars` (default `50_000`) is a 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.
|
|
295
|
-
|
|
296
|
-
```python
|
|
297
|
-
CodeModeCodeExecutor(tools=..., backend=..., max_catalog_chars=20_000)
|
|
298
|
-
```
|
|
307
|
+
All settings are `CodeModeCodeExecutor` constructor arguments:
|
|
299
308
|
|
|
300
|
-
|
|
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`. |
|
|
301
317
|
|
|
302
|
-
|
|
318
|
+
The model can read spilled stdout back from the overflow artifact:
|
|
303
319
|
|
|
304
320
|
```python
|
|
305
321
|
from tools import load_artifact
|
|
@@ -307,26 +323,13 @@ spilled = load_artifact(filename="code_mode/stdout/<execution-id>.txt")
|
|
|
307
323
|
print(spilled["data"][-2000:])
|
|
308
324
|
```
|
|
309
325
|
|
|
310
|
-
###
|
|
326
|
+
### Turn-scoped sessions
|
|
311
327
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
### Timeouts
|
|
315
|
-
|
|
316
|
-
`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:
|
|
317
|
-
|
|
318
|
-
```python
|
|
319
|
-
CodeModeCodeExecutor(
|
|
320
|
-
tools=...,
|
|
321
|
-
backend=...,
|
|
322
|
-
timeout_seconds=30,
|
|
323
|
-
per_tool_timeout_seconds=10,
|
|
324
|
-
)
|
|
325
|
-
```
|
|
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.
|
|
326
329
|
|
|
327
330
|
## 🏗️ Architecture
|
|
328
331
|
|
|
329
|
-
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. The `before_model_callback` resolves tools, renders the catalog, and appends it to the system prompt. At execution time, it generates a `tools/` Python package of thin stubs, stages `input_files` into `/workspace`, and
|
|
332
|
+
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. The `before_model_callback` resolves tools, renders the catalog, and appends it to the system prompt. At execution time, it generates a `tools/` Python package of thin stubs, stages the block's `input_files` into `/workspace`, and opens (or reuses) the turn's sandbox connection — the container spans the whole turn.
|
|
330
333
|
|
|
331
334
|
**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.
|
|
332
335
|
|
|
@@ -341,37 +344,40 @@ The only things crossing the boundary are: code, tool call arguments, tool retur
|
|
|
341
344
|
|
|
342
345
|
Your `instruction` (containing `CODE_MODE_SYSTEM_INSTRUCTION`) followed by a `<code-mode>` block appended by the callback:
|
|
343
346
|
|
|
344
|
-
|
|
347
|
+
~~~
|
|
345
348
|
…your instruction…
|
|
346
349
|
|
|
347
350
|
# How to execute code and use tools
|
|
348
|
-
|
|
349
|
-
The Python Standard Library and a
|
|
350
|
-
To see the result of your code, you need to print it.
|
|
351
|
-
|
|
352
|
-
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:
|
|
353
354
|
|
|
354
355
|
```
|
|
356
|
+
<code-mode>
|
|
357
|
+
|
|
355
358
|
from tools.slack import send_message
|
|
356
359
|
|
|
357
360
|
def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
|
|
358
361
|
"""Send a message to a Slack channel."""
|
|
359
362
|
...
|
|
363
|
+
|
|
364
|
+
</code-mode>
|
|
360
365
|
```
|
|
361
366
|
|
|
362
367
|
To call the tool, you should write:
|
|
363
368
|
|
|
364
|
-
|
|
369
|
+
````
|
|
365
370
|
```python
|
|
366
371
|
from tools.slack import send_message
|
|
367
372
|
|
|
368
373
|
print(send_message(channel="C123", text="hi"))
|
|
369
374
|
```
|
|
370
|
-
|
|
375
|
+
````
|
|
371
376
|
|
|
372
377
|
# How to use files and variables in between executions
|
|
373
|
-
|
|
374
|
-
To list available Artifacts, use the `list_artifacts` tool
|
|
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.
|
|
375
381
|
|
|
376
382
|
<code-mode>
|
|
377
383
|
|
|
@@ -393,7 +399,9 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
393
399
|
…
|
|
394
400
|
|
|
395
401
|
</code-mode>
|
|
396
|
-
|
|
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.
|
|
397
405
|
|
|
398
406
|
When the rendered catalog exceeds `max_catalog_chars`, the per-tool sections are replaced with:
|
|
399
407
|
|
|
@@ -416,7 +424,7 @@ Text and JSON-like MIME types travel as plain strings in artifact tools; binary
|
|
|
416
424
|
|
|
417
425
|
`RemoteBackend` is designed for multi-tenant production use where untrusted users submit arbitrary Python code:
|
|
418
426
|
|
|
419
|
-
- **One container per
|
|
427
|
+
- **One container per turn (one tenant, one invocation).** Within a turn the process/filesystem are reused across that turn's code blocks; the container is destroyed at turn end, with **no cross-turn or cross-tenant sharing**.
|
|
420
428
|
- **Environment sanitization.** All env vars are stripped except a safe allowlist (`PATH`, `HOME`, `USER`, locale vars, Python config) before user code runs.
|
|
421
429
|
- **Credentials never enter the sandbox.** API keys, OAuth tokens, and connection strings stay in the host process. The container only receives tool results.
|
|
422
430
|
- **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.
|
|
@@ -441,7 +449,7 @@ Named "Unsafe" intentionally: it binds a TCP listener on `0.0.0.0`, communicates
|
|
|
441
449
|
## ⚠️ Limitations
|
|
442
450
|
|
|
443
451
|
- **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.
|
|
444
|
-
- **
|
|
452
|
+
- **State is turn-scoped.** Variables and `/workspace` files persist across code blocks **within** a turn, but reset between turns. Use `save_artifact` / `load_artifact` to persist across turns.
|
|
445
453
|
- **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.
|
|
446
454
|
|
|
447
455
|
## 🛠️ Development
|