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.
Files changed (79) hide show
  1. adk_code_mode-0.4.0/.release-please-manifest.json +3 -0
  2. adk_code_mode-0.4.0/CHANGELOG.md +77 -0
  3. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/PKG-INFO +68 -60
  4. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/README.md +67 -59
  5. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/docker/Dockerfile +2 -2
  6. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
  7. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +38 -9
  8. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +109 -74
  9. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +6 -2
  10. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/__about__.py +1 -1
  11. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/__init__.py +14 -0
  12. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/callback.py +1 -1
  13. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/executor.py +449 -92
  14. adk_code_mode-0.4.0/src/adk_code_mode/runtime/base.py +126 -0
  15. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/docker.py +56 -33
  16. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/protocol.py +6 -2
  17. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/remote.py +116 -25
  18. adk_code_mode-0.4.0/src/adk_code_mode/tool_result_artifacts.py +187 -0
  19. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/_fake_runtime.py +42 -12
  20. adk_code_mode-0.4.0/tests/conftest.py +37 -0
  21. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_docker_integration.py +43 -0
  22. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_host_loop.py +4 -1
  23. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_integration.py +345 -1
  24. adk_code_mode-0.4.0/tests/test_remote_backend.py +82 -0
  25. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_remote_integration.py +149 -1
  26. adk_code_mode-0.4.0/tests/test_tool_result_artifacts.py +177 -0
  27. adk_code_mode-0.2.2/.release-please-manifest.json +0 -3
  28. adk_code_mode-0.2.2/CHANGELOG.md +0 -44
  29. adk_code_mode-0.2.2/src/adk_code_mode/runtime/base.py +0 -88
  30. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.githooks/pre-commit +0 -0
  31. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.githooks/pre-push +0 -0
  32. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.github/workflows/ci.yml +0 -0
  33. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.github/workflows/release-please.yml +0 -0
  34. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.gitignore +0 -0
  35. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.prettierrc +0 -0
  36. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.vscode/settings.json +0 -0
  37. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/.zed/settings.json +0 -0
  38. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/AGENTS.md +0 -0
  39. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/CLAUDE.md +0 -0
  40. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/LICENSE +0 -0
  41. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/Makefile +0 -0
  42. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/pyproject.toml +0 -0
  43. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/release-please-config.json +0 -0
  44. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/README.md +0 -0
  45. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/pyproject.toml +0 -0
  46. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
  47. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
  48. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
  49. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/scripts/install-git-hooks.sh +0 -0
  50. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/_artifact_tools.py +0 -0
  51. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/output.py +0 -0
  52. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/py.typed +0 -0
  53. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/runtime/__init__.py +0 -0
  54. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/__init__.py +0 -0
  55. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/catalog.py +0 -0
  56. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
  57. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/namespacing.py +0 -0
  58. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/normaliser.py +0 -0
  59. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/tools/stubs.py +0 -0
  60. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/workspace/__init__.py +0 -0
  61. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/src/adk_code_mode/workspace/files.py +0 -0
  62. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/__init__.py +0 -0
  63. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/_docker_helpers.py +0 -0
  64. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_agent_with_fake_llm.py +0 -0
  65. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_artifact_tools.py +0 -0
  66. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_callback.py +0 -0
  67. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_catalog.py +0 -0
  68. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_dispatcher.py +0 -0
  69. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_docker_image.py +0 -0
  70. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_namespacing.py +0 -0
  71. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_normaliser.py +0 -0
  72. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_output.py +0 -0
  73. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_protocol.py +0 -0
  74. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_runtime_docker.py +0 -0
  75. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_sandbox_rpc_client.py +0 -0
  76. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_stubs.py +0 -0
  77. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_version.py +0 -0
  78. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/tests/test_workspace_files.py +0 -0
  79. {adk_code_mode-0.2.2 → adk_code_mode-0.4.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -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.2.2
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, single-use container over WebSocket. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
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 | 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 three things into the agent:
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 execution runs in its own container.** The container accepts exactly one WebSocket connection, executes the user's code, returns results, and exits. The hosting platform destroys the container after each request no cross-tenant data leakage, no residual state. You **must** configure your platform for one container per request (`--concurrency 1` on Cloud Run, or equivalent).
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 and --vpc-egress=all-traffic
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 requests to the same container. The sandbox rejects the second connection, but the misconfiguration itself is a risk.
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 request.** Each container handles exactly one execution and exits.
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 activate HTTP mode |
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
- The same upload limits (plus a download limit) are configurable on `RemoteBackend`:
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`** — per-run working directory. ADK `input_files` are staged here before code runs (`open("input.csv")` works). Files created or modified under `/workspace` are returned as `CodeExecutionResult.output_files` but are not re-hydrated next turn unless persisted via `save_artifact`.
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
- ### Catalog overflow
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
- ### Output truncation
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
- `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:
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
- ### Code size limit
326
+ ### Turn-scoped sessions
311
327
 
312
- `max_code_chars` (default `1_000_000`) rejects oversized code payloads before starting a container.
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 launches the sandbox.
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
- Code you write in a fenced Python block (i.e. ```python) will be executed in a sandbox.
349
- The Python Standard Library and a custom set of tools are available to you.
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
- Code is executed in a new environment each time.
374
- 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.
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 execution.** Fresh container per request no shared filesystem, memory, or processes.
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
- - **No state across executions.** Variables don't survive between turns. Use `save_artifact` / `load_artifact` to persist, or `/workspace` within a single run.
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