adk-code-mode 1.1.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adk_code_mode-1.3.0/.release-please-manifest.json +3 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/CHANGELOG.md +14 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/PKG-INFO +59 -12
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/README.md +58 -11
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +37 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +4 -3
- {adk_code_mode-1.1.0/src/adk_code_mode/runtime → adk_code_mode-1.3.0/sandbox-wheel/src/adk_code_mode_sandbox}/protocol.py +11 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/__about__.py +1 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/_artifact_tools.py +4 -1
- adk_code_mode-1.3.0/src/adk_code_mode/metadata.py +149 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/runtime/docker.py +5 -0
- {adk_code_mode-1.1.0/sandbox-wheel/src/adk_code_mode_sandbox → adk_code_mode-1.3.0/src/adk_code_mode/runtime}/protocol.py +11 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/runtime/remote.py +5 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tool.py +73 -44
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/catalog.py +20 -7
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_agent_with_fake_llm.py +2 -2
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_artifact_tools.py +42 -8
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_docker_integration.py +34 -1
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_host_loop.py +22 -2
- adk_code_mode-1.3.0/tests/test_metadata.py +207 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_protocol.py +6 -1
- adk_code_mode-1.3.0/tests/test_sandbox_entry.py +52 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_tool.py +36 -8
- adk_code_mode-1.1.0/.release-please-manifest.json +0 -3
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.githooks/pre-commit +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.githooks/pre-push +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.gitignore +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.prettierrc +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.vscode/settings.json +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/.zed/settings.json +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/AGENTS.md +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/CLAUDE.md +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/LICENSE +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/Makefile +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/docker/Dockerfile +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/pyproject.toml +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/release-please-config.json +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/output.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/runtime/base.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tool_result_artifacts.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/namespacing.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/normaliser.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/tools/stubs.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/workspace/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/src/adk_code_mode/workspace/files.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/__init__.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/_fake_runtime.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/conftest.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_catalog.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_dispatcher.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_docker_image.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_integration.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_namespacing.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_normaliser.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_output.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_remote_backend.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_remote_integration.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_stubs.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_tool_result_artifacts.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_version.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/tests/test_workspace_files.py +0 -0
- {adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/uv.lock +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.2.0...adk-code-mode-v1.3.0) (2026-07-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* describe the sandbox to the model in a tagged `<code-mode>` block ([#14](https://github.com/a2anet/adk-code-mode/issues/14)) ([662a281](https://github.com/a2anet/adk-code-mode/commit/662a28138e8632b434e5f1ecd52793f7d83ee097))
|
|
9
|
+
|
|
10
|
+
## [1.2.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.1.0...adk-code-mode-v1.2.0) (2026-07-23)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add `custom_metadata` to the `save_artifact` tool ([dfd9a59](https://github.com/a2anet/adk-code-mode/commit/dfd9a59bf1718084a1d5c02cc1ea453269223f16))
|
|
16
|
+
|
|
3
17
|
## [1.1.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.0.0...adk-code-mode-v1.1.0) (2026-07-15)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A "Code Mode" sandboxed code-execution tool for ADK agents to interact with tools, files, and custom packages with Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/A2ANet/adk-code-mode#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/A2ANet/adk-code-mode/issues
|
|
@@ -39,7 +39,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
39
39
|
## ✨ Features
|
|
40
40
|
|
|
41
41
|
- **Call ADK tools from sandbox code** — imports against the `tools` package proxy back to the host and run through ADK's `before_tool` / `after_tool` / `on_error` callbacks and the plugin manager exactly as direct tool calls would.
|
|
42
|
-
- **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.
|
|
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. The sandbox reports what it has installed, so the model is told about it automatically.
|
|
43
43
|
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`. Files the code creates or changes are saved as artifacts automatically too.
|
|
44
44
|
- **Tool results saved as artifacts** — on by default; every tool's result is persisted as a `code_mode.tool_result` artifact (with optional model-supplied name/description) so hosts can forward outputs and large results stay out of the prompt. Opt out with `save_tool_results_as_artifacts=False`.
|
|
45
45
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
@@ -280,14 +280,16 @@ RUN pip install --no-cache-dir pandas==2.2.*
|
|
|
280
280
|
|
|
281
281
|
The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To build directly from this repo, run `make docker-image`.
|
|
282
282
|
|
|
283
|
+
Whatever you install is advertised to the model automatically — no host-side configuration. The sandbox reports its Python version and maps each top-level import name to the installed distribution and version that provides it; they appear in the `<code-mode>` block as `<python-version>` and `<installed-packages>`. Because the report only arrives once a container has booted, the first model call of a host process doesn't carry them yet; from then on the values are cached per sandbox image for the life of the process.
|
|
284
|
+
|
|
283
285
|
## ⚙️ Configuration
|
|
284
286
|
|
|
285
287
|
All settings are `ExecuteCodeTool` constructor arguments:
|
|
286
288
|
|
|
287
289
|
| Argument | Default | Purpose |
|
|
288
290
|
| --- | --- | --- |
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
+
| `append_code_mode_metadata_to_system_instruction` | `True` | Appends the `<code-mode>` block — Python version, installed packages, and the function catalog — to the system instruction on every model turn. Set `False` for a bare system instruction; the tool description then points the model at `/tools/` instead, and the Python version and package list are not surfaced at all. |
|
|
292
|
+
| `max_code_mode_metadata_chars` | `50_000` | Budget for the whole rendered `<code-mode>` block, tags included. Oversized blocks degrade in tiers rather than disappearing — see [What the model sees](#what-the-model-sees). |
|
|
291
293
|
| `max_output_chars` | `50_000` | Caps stdout/stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<call-id>.txt` and the model sees a head-and-tail view pointing to it. |
|
|
292
294
|
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
293
295
|
| `timeout_seconds` | `None` | Caps overall execution time of one `execute_code` call. Defaults to the platform request timeout (e.g. Cloud Run `--timeout`); set explicitly for defense in depth. |
|
|
@@ -308,7 +310,7 @@ A sandbox container is held open for one **turn** (one ADK invocation) and reuse
|
|
|
308
310
|
|
|
309
311
|
## 🏗️ Architecture
|
|
310
312
|
|
|
311
|
-
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. `ExecuteCodeTool.process_llm_request` resolves tools and, when `
|
|
313
|
+
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. `ExecuteCodeTool.process_llm_request` resolves tools and, when `append_code_mode_metadata_to_system_instruction` is enabled, renders the `<code-mode>` block and appends it to the system instruction. At execution time (`run_async`), it generates a `tools/` Python package of thin stubs, stages the working directory, and opens (or reuses) the turn's sandbox connection — the container spans the whole turn.
|
|
312
314
|
|
|
313
315
|
**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.
|
|
314
316
|
|
|
@@ -321,16 +323,36 @@ The only things crossing the boundary are: code, tool call arguments, tool retur
|
|
|
321
323
|
|
|
322
324
|
### What the model sees
|
|
323
325
|
|
|
324
|
-
|
|
326
|
+
Two surfaces, deliberately kept apart. The **tool description** is the invariant contract of calling `execute_code` — the same for every deployment, and the model's only map when the `<code-mode>` block is disabled. `execute_code` has a single `code: string` parameter and this description:
|
|
325
327
|
|
|
326
328
|
~~~
|
|
327
|
-
|
|
329
|
+
Execute Python code in a sandboxed container. Custom tools are Python functions to
|
|
330
|
+
import from the `tools` package (e.g. `from tools.slack import send_message`). See
|
|
331
|
+
the `<code-mode>` section of the system instruction for the Python version,
|
|
332
|
+
preinstalled packages, and available functions. Print anything you want to see; only
|
|
333
|
+
stdout/stderr are returned. Variables and the working directory persist across calls
|
|
334
|
+
within the same turn and reset on the next turn — use `save_artifact` /
|
|
335
|
+
`load_artifact` (imported the same way) to persist across turns. Files created or
|
|
336
|
+
changed by the code are saved as artifacts automatically
|
|
337
|
+
~~~
|
|
328
338
|
|
|
329
|
-
|
|
330
|
-
Import these functions from the `tools` package in the code you pass to execute_code.
|
|
339
|
+
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by `List /tools/ and read a file's docstring to see what's available.`, so the description never points at a section that wasn't appended.
|
|
331
340
|
|
|
332
|
-
|
|
341
|
+
The **`<code-mode>` block** is the per-deployment inventory of the sandbox, appended to the system instruction on every model turn:
|
|
333
342
|
|
|
343
|
+
~~~
|
|
344
|
+
…your instruction…
|
|
345
|
+
|
|
346
|
+
<code-mode>
|
|
347
|
+
<how-to-use>
|
|
348
|
+
This section describes the sandbox that `execute_code` runs in: the Python version available, the third-party packages preinstalled in it, and the functions you can import from the `tools` package.
|
|
349
|
+
</how-to-use>
|
|
350
|
+
<python-version>3.13.2</python-version>
|
|
351
|
+
<installed-packages>
|
|
352
|
+
pandas: pandas 2.3.3
|
|
353
|
+
yaml: PyYAML 6.0.2
|
|
354
|
+
</installed-packages>
|
|
355
|
+
<tools-package>
|
|
334
356
|
# tools.slack
|
|
335
357
|
|
|
336
358
|
from tools.slack import list_channels, send_message
|
|
@@ -347,13 +369,38 @@ def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any
|
|
|
347
369
|
|
|
348
370
|
from tools import save_artifact, load_artifact, list_artifacts
|
|
349
371
|
…
|
|
350
|
-
|
|
372
|
+
</tools-package>
|
|
351
373
|
</code-mode>
|
|
352
374
|
~~~
|
|
353
375
|
|
|
376
|
+
Tags are indented; their content is not. The `<tools-package>` body is Python source, where leading whitespace is meaningful. Each installed-package line is `<import name>: <distribution> <version>`; namespace imports supplied by multiple distributions list every provider in deterministic order. Import names and providers are sorted so the block stays byte-stable for prompt caching. `<python-version>` and `<installed-packages>` are omitted entirely — rather than rendered blank — when the sandbox hasn't reported yet or the image has no extra packages, since an empty tag reads as "none exist".
|
|
377
|
+
|
|
354
378
|
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.
|
|
355
379
|
|
|
356
|
-
|
|
380
|
+
#### Fitting the budget
|
|
381
|
+
|
|
382
|
+
A block over `max_code_mode_metadata_chars` degrades in tiers instead of vanishing. The Python version and package list survive every tier — they're small, and the model cannot discover them any other way.
|
|
383
|
+
|
|
384
|
+
1. **Full** — as above.
|
|
385
|
+
2. **Names only** — signatures and docstrings dropped, import lines kept:
|
|
386
|
+
|
|
387
|
+
~~~
|
|
388
|
+
<tools-package>
|
|
389
|
+
# tools.slack
|
|
390
|
+
|
|
391
|
+
from tools.slack import list_channels, send_message
|
|
392
|
+
</tools-package>
|
|
393
|
+
~~~
|
|
394
|
+
|
|
395
|
+
3. **Pointer** — the catalog is replaced by a discovery note, and this tier is emitted no matter how small the budget:
|
|
396
|
+
|
|
397
|
+
~~~
|
|
398
|
+
<tools-package>
|
|
399
|
+
The full catalog is too large to include here. List `/tools/` and read a file's docstring to see what's available.
|
|
400
|
+
</tools-package>
|
|
401
|
+
~~~
|
|
402
|
+
|
|
403
|
+
At the last tier the model navigates the sandbox from Python instead:
|
|
357
404
|
|
|
358
405
|
```python
|
|
359
406
|
import pathlib
|
|
@@ -12,7 +12,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
12
12
|
## ✨ Features
|
|
13
13
|
|
|
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
|
-
- **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.
|
|
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. The sandbox reports what it has installed, so the model is told about it automatically.
|
|
16
16
|
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`. Files the code creates or changes are saved as artifacts automatically too.
|
|
17
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`.
|
|
18
18
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
@@ -253,14 +253,16 @@ RUN pip install --no-cache-dir pandas==2.2.*
|
|
|
253
253
|
|
|
254
254
|
The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To build directly from this repo, run `make docker-image`.
|
|
255
255
|
|
|
256
|
+
Whatever you install is advertised to the model automatically — no host-side configuration. The sandbox reports its Python version and maps each top-level import name to the installed distribution and version that provides it; they appear in the `<code-mode>` block as `<python-version>` and `<installed-packages>`. Because the report only arrives once a container has booted, the first model call of a host process doesn't carry them yet; from then on the values are cached per sandbox image for the life of the process.
|
|
257
|
+
|
|
256
258
|
## ⚙️ Configuration
|
|
257
259
|
|
|
258
260
|
All settings are `ExecuteCodeTool` constructor arguments:
|
|
259
261
|
|
|
260
262
|
| Argument | Default | Purpose |
|
|
261
263
|
| --- | --- | --- |
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
+
| `append_code_mode_metadata_to_system_instruction` | `True` | Appends the `<code-mode>` block — Python version, installed packages, and the function catalog — to the system instruction on every model turn. Set `False` for a bare system instruction; the tool description then points the model at `/tools/` instead, and the Python version and package list are not surfaced at all. |
|
|
265
|
+
| `max_code_mode_metadata_chars` | `50_000` | Budget for the whole rendered `<code-mode>` block, tags included. Oversized blocks degrade in tiers rather than disappearing — see [What the model sees](#what-the-model-sees). |
|
|
264
266
|
| `max_output_chars` | `50_000` | Caps stdout/stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<call-id>.txt` and the model sees a head-and-tail view pointing to it. |
|
|
265
267
|
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
266
268
|
| `timeout_seconds` | `None` | Caps overall execution time of one `execute_code` call. Defaults to the platform request timeout (e.g. Cloud Run `--timeout`); set explicitly for defense in depth. |
|
|
@@ -281,7 +283,7 @@ A sandbox container is held open for one **turn** (one ADK invocation) and reuse
|
|
|
281
283
|
|
|
282
284
|
## 🏗️ Architecture
|
|
283
285
|
|
|
284
|
-
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. `ExecuteCodeTool.process_llm_request` resolves tools and, when `
|
|
286
|
+
**Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. `ExecuteCodeTool.process_llm_request` resolves tools and, when `append_code_mode_metadata_to_system_instruction` is enabled, renders the `<code-mode>` block and appends it to the system instruction. At execution time (`run_async`), it generates a `tools/` Python package of thin stubs, stages the working directory, and opens (or reuses) the turn's sandbox connection — the container spans the whole turn.
|
|
285
287
|
|
|
286
288
|
**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.
|
|
287
289
|
|
|
@@ -294,16 +296,36 @@ The only things crossing the boundary are: code, tool call arguments, tool retur
|
|
|
294
296
|
|
|
295
297
|
### What the model sees
|
|
296
298
|
|
|
297
|
-
|
|
299
|
+
Two surfaces, deliberately kept apart. The **tool description** is the invariant contract of calling `execute_code` — the same for every deployment, and the model's only map when the `<code-mode>` block is disabled. `execute_code` has a single `code: string` parameter and this description:
|
|
298
300
|
|
|
299
301
|
~~~
|
|
300
|
-
|
|
302
|
+
Execute Python code in a sandboxed container. Custom tools are Python functions to
|
|
303
|
+
import from the `tools` package (e.g. `from tools.slack import send_message`). See
|
|
304
|
+
the `<code-mode>` section of the system instruction for the Python version,
|
|
305
|
+
preinstalled packages, and available functions. Print anything you want to see; only
|
|
306
|
+
stdout/stderr are returned. Variables and the working directory persist across calls
|
|
307
|
+
within the same turn and reset on the next turn — use `save_artifact` /
|
|
308
|
+
`load_artifact` (imported the same way) to persist across turns. Files created or
|
|
309
|
+
changed by the code are saved as artifacts automatically
|
|
310
|
+
~~~
|
|
301
311
|
|
|
302
|
-
|
|
303
|
-
Import these functions from the `tools` package in the code you pass to execute_code.
|
|
312
|
+
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by `List /tools/ and read a file's docstring to see what's available.`, so the description never points at a section that wasn't appended.
|
|
304
313
|
|
|
305
|
-
|
|
314
|
+
The **`<code-mode>` block** is the per-deployment inventory of the sandbox, appended to the system instruction on every model turn:
|
|
306
315
|
|
|
316
|
+
~~~
|
|
317
|
+
…your instruction…
|
|
318
|
+
|
|
319
|
+
<code-mode>
|
|
320
|
+
<how-to-use>
|
|
321
|
+
This section describes the sandbox that `execute_code` runs in: the Python version available, the third-party packages preinstalled in it, and the functions you can import from the `tools` package.
|
|
322
|
+
</how-to-use>
|
|
323
|
+
<python-version>3.13.2</python-version>
|
|
324
|
+
<installed-packages>
|
|
325
|
+
pandas: pandas 2.3.3
|
|
326
|
+
yaml: PyYAML 6.0.2
|
|
327
|
+
</installed-packages>
|
|
328
|
+
<tools-package>
|
|
307
329
|
# tools.slack
|
|
308
330
|
|
|
309
331
|
from tools.slack import list_channels, send_message
|
|
@@ -320,13 +342,38 @@ def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any
|
|
|
320
342
|
|
|
321
343
|
from tools import save_artifact, load_artifact, list_artifacts
|
|
322
344
|
…
|
|
323
|
-
|
|
345
|
+
</tools-package>
|
|
324
346
|
</code-mode>
|
|
325
347
|
~~~
|
|
326
348
|
|
|
349
|
+
Tags are indented; their content is not. The `<tools-package>` body is Python source, where leading whitespace is meaningful. Each installed-package line is `<import name>: <distribution> <version>`; namespace imports supplied by multiple distributions list every provider in deterministic order. Import names and providers are sorted so the block stays byte-stable for prompt caching. `<python-version>` and `<installed-packages>` are omitted entirely — rather than rendered blank — when the sandbox hasn't reported yet or the image has no extra packages, since an empty tag reads as "none exist".
|
|
350
|
+
|
|
327
351
|
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.
|
|
328
352
|
|
|
329
|
-
|
|
353
|
+
#### Fitting the budget
|
|
354
|
+
|
|
355
|
+
A block over `max_code_mode_metadata_chars` degrades in tiers instead of vanishing. The Python version and package list survive every tier — they're small, and the model cannot discover them any other way.
|
|
356
|
+
|
|
357
|
+
1. **Full** — as above.
|
|
358
|
+
2. **Names only** — signatures and docstrings dropped, import lines kept:
|
|
359
|
+
|
|
360
|
+
~~~
|
|
361
|
+
<tools-package>
|
|
362
|
+
# tools.slack
|
|
363
|
+
|
|
364
|
+
from tools.slack import list_channels, send_message
|
|
365
|
+
</tools-package>
|
|
366
|
+
~~~
|
|
367
|
+
|
|
368
|
+
3. **Pointer** — the catalog is replaced by a discovery note, and this tier is emitted no matter how small the budget:
|
|
369
|
+
|
|
370
|
+
~~~
|
|
371
|
+
<tools-package>
|
|
372
|
+
The full catalog is too large to include here. List `/tools/` and read a file's docstring to see what's available.
|
|
373
|
+
</tools-package>
|
|
374
|
+
~~~
|
|
375
|
+
|
|
376
|
+
At the last tier the model navigates the sandbox from Python instead:
|
|
330
377
|
|
|
331
378
|
```python
|
|
332
379
|
import pathlib
|
{adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py
RENAMED
|
@@ -23,8 +23,10 @@ its stdout/stderr captured and shipped back in a per-block ``OutputFrame``; any
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
import importlib.metadata
|
|
26
27
|
import io
|
|
27
28
|
import os
|
|
29
|
+
import platform
|
|
28
30
|
import socket
|
|
29
31
|
import sys
|
|
30
32
|
import traceback
|
|
@@ -40,6 +42,11 @@ from adk_code_mode_sandbox.protocol import (
|
|
|
40
42
|
ShutdownFrame,
|
|
41
43
|
)
|
|
42
44
|
|
|
45
|
+
# Distributions that ship with the sandbox itself rather than being installed by
|
|
46
|
+
# whoever built the image. Reporting them would tell the model it has tools it
|
|
47
|
+
# was never meant to use (`websockets` is this wheel's only dependency).
|
|
48
|
+
_PACKAGE_DENYLIST = frozenset({"adk-code-mode-sandbox", "websockets", "pip", "setuptools", "wheel"})
|
|
49
|
+
|
|
43
50
|
CONTROL_FD_ENV = "ADK_CODE_MODE_CONTROL_FD"
|
|
44
51
|
CONTROL_SOCKET_ENV = "ADK_CODE_MODE_CONTROL_SOCKET"
|
|
45
52
|
CONTROL_TCP_ENV = "ADK_CODE_MODE_CONTROL_TCP"
|
|
@@ -50,6 +57,35 @@ TOOLS_DIR = os.environ.get(TOOLS_DIR_ENV, "/tools")
|
|
|
50
57
|
WORKDIR = os.environ.get(WORKDIR_ENV, "/workspace")
|
|
51
58
|
|
|
52
59
|
|
|
60
|
+
def ready_frame() -> ReadyFrame:
|
|
61
|
+
"""Build the ``ReadyFrame`` announcing this image's Python and packages.
|
|
62
|
+
|
|
63
|
+
Both transports send this, so image introspection stays in one place.
|
|
64
|
+
"""
|
|
65
|
+
distributions = {
|
|
66
|
+
dist.name.casefold(): (dist.name, dist.version)
|
|
67
|
+
for dist in sorted(
|
|
68
|
+
importlib.metadata.distributions(),
|
|
69
|
+
key=lambda dist: (dist.name.casefold(), dist.name, dist.version),
|
|
70
|
+
)
|
|
71
|
+
if dist.name.casefold() not in _PACKAGE_DENYLIST
|
|
72
|
+
}
|
|
73
|
+
packages: dict[str, dict[str, str]] = {}
|
|
74
|
+
package_distributions = importlib.metadata.packages_distributions()
|
|
75
|
+
for import_name in sorted(package_distributions, key=lambda name: (name.casefold(), name)):
|
|
76
|
+
providers: dict[str, str] = {}
|
|
77
|
+
for distribution_name in sorted(
|
|
78
|
+
package_distributions[import_name],
|
|
79
|
+
key=lambda name: (name.casefold(), name),
|
|
80
|
+
):
|
|
81
|
+
distribution = distributions.get(distribution_name.casefold())
|
|
82
|
+
if distribution is not None:
|
|
83
|
+
providers[distribution[0]] = distribution[1]
|
|
84
|
+
if providers:
|
|
85
|
+
packages[import_name] = providers
|
|
86
|
+
return ReadyFrame(python_version=platform.python_version(), packages=packages)
|
|
87
|
+
|
|
88
|
+
|
|
53
89
|
def _open_control_streams() -> tuple[Any, Any]:
|
|
54
90
|
"""Open the host control pipe as a (reader, writer) pair."""
|
|
55
91
|
tcp_endpoint = os.environ.get(CONTROL_TCP_ENV)
|
|
@@ -204,7 +240,7 @@ def main() -> int:
|
|
|
204
240
|
# One persistent globals dict + one /workspace for the whole connection, so
|
|
205
241
|
# variables and files carry across the turn's successive code blocks.
|
|
206
242
|
globs = _make_globals()
|
|
207
|
-
client.send(
|
|
243
|
+
client.send(ready_frame())
|
|
208
244
|
|
|
209
245
|
while True:
|
|
210
246
|
frame = client.recv()
|
{adk_code_mode-1.1.0 → adk_code_mode-1.3.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py
RENAMED
|
@@ -49,6 +49,7 @@ from adk_code_mode_sandbox._entry import (
|
|
|
49
49
|
_prepare_sys_path,
|
|
50
50
|
_prepare_workdir,
|
|
51
51
|
_sanitize_environ,
|
|
52
|
+
ready_frame,
|
|
52
53
|
run_block,
|
|
53
54
|
)
|
|
54
55
|
from adk_code_mode_sandbox._rpc_client import RpcClient
|
|
@@ -57,7 +58,6 @@ from adk_code_mode_sandbox.protocol import (
|
|
|
57
58
|
DoneFrame,
|
|
58
59
|
OutputFrame,
|
|
59
60
|
ProtocolError,
|
|
60
|
-
ReadyFrame,
|
|
61
61
|
RunFrame,
|
|
62
62
|
ShutdownFrame,
|
|
63
63
|
decode,
|
|
@@ -188,8 +188,9 @@ async def _handle_connection(ws: Any) -> None:
|
|
|
188
188
|
globs = _make_globals()
|
|
189
189
|
loop = asyncio.get_running_loop()
|
|
190
190
|
|
|
191
|
-
# 4. Announce readiness once (carries the protocol version
|
|
192
|
-
|
|
191
|
+
# 4. Announce readiness once (carries the protocol version, Python
|
|
192
|
+
# version, and installed packages).
|
|
193
|
+
await ws.send(encode(ready_frame()).decode("utf-8"))
|
|
193
194
|
|
|
194
195
|
# 5. Run code blocks until the client ends the turn or disconnects.
|
|
195
196
|
while await _run_one_block(ws, loop, globs, workspace_dir, max_workspace):
|
|
@@ -39,10 +39,20 @@ FrameKind = Literal[
|
|
|
39
39
|
|
|
40
40
|
@dataclass(frozen=True)
|
|
41
41
|
class ReadyFrame:
|
|
42
|
-
"""Sandbox → host. Sent once after boot.
|
|
42
|
+
"""Sandbox → host. Sent once after boot.
|
|
43
|
+
|
|
44
|
+
``python_version`` / ``packages`` describe the image the sandbox runs in so
|
|
45
|
+
the host can tell the model what it can import. ``packages`` maps each
|
|
46
|
+
top-level import name to the distributions that provide it and their
|
|
47
|
+
versions. Both are additive fields with defaults, so a host and a sandbox
|
|
48
|
+
on different releases still speak: ``decode`` drops unknown keys, and an
|
|
49
|
+
older sandbox that sends neither just leaves them empty.
|
|
50
|
+
"""
|
|
43
51
|
|
|
44
52
|
kind: Literal["ready"] = "ready"
|
|
45
53
|
protocol_version: int = PROTOCOL_VERSION
|
|
54
|
+
python_version: str = ""
|
|
55
|
+
packages: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
46
56
|
|
|
47
57
|
|
|
48
58
|
@dataclass(frozen=True)
|
|
@@ -43,6 +43,7 @@ async def save_artifact(
|
|
|
43
43
|
filename: str,
|
|
44
44
|
content: str,
|
|
45
45
|
mime_type: str | None = None,
|
|
46
|
+
custom_metadata: dict[str, Any] | None = None,
|
|
46
47
|
tool_context: ToolContext,
|
|
47
48
|
) -> int:
|
|
48
49
|
"""Save an artifact to the session. Returns the new version number.
|
|
@@ -62,6 +63,8 @@ async def save_artifact(
|
|
|
62
63
|
content=base64.b64encode(image_bytes).decode("ascii"),
|
|
63
64
|
mime_type="image/png",
|
|
64
65
|
)
|
|
66
|
+
|
|
67
|
+
``custom_metadata`` is custom metadata to associate with the artifact.
|
|
65
68
|
"""
|
|
66
69
|
if _is_text_mime(mime_type):
|
|
67
70
|
part = genai_types.Part(
|
|
@@ -77,7 +80,7 @@ async def save_artifact(
|
|
|
77
80
|
mime_type=mime_type or "application/octet-stream",
|
|
78
81
|
)
|
|
79
82
|
)
|
|
80
|
-
return await tool_context.save_artifact(filename, part)
|
|
83
|
+
return await tool_context.save_artifact(filename, part, custom_metadata=custom_metadata)
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
async def load_artifact(
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Assemble the ``<code-mode>`` block appended to the model's system instruction.
|
|
5
|
+
|
|
6
|
+
The block is the sandbox's *inventory* — which Python, which preinstalled
|
|
7
|
+
packages, which importable functions. The invariant contract of calling
|
|
8
|
+
``execute_code`` lives in the tool's description instead, so the two never
|
|
9
|
+
restate each other.
|
|
10
|
+
|
|
11
|
+
The Python version and package list are reported by the sandbox itself in its
|
|
12
|
+
``ReadyFrame``, which only arrives once a container has booted — i.e. after the
|
|
13
|
+
first ``execute_code`` call. They are cached here, keyed by the backend's
|
|
14
|
+
``identity`` (its URL or image) rather than on ``ExecuteCodeTool``, so one warm
|
|
15
|
+
process serves every agent pointing at the same image and the value survives a
|
|
16
|
+
tool being rebuilt. Recording is monotonic and rendering is sorted: the block
|
|
17
|
+
must be byte-stable once known, because it sits in front of the whole prompt
|
|
18
|
+
and any churn re-writes the model provider's cached prefix.
|
|
19
|
+
|
|
20
|
+
Tags are indented; their text content is not. The ``<tools-package>`` body is
|
|
21
|
+
Python source, where leading whitespace is meaningful.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from adk_code_mode.runtime.protocol import ReadyFrame
|
|
29
|
+
from adk_code_mode.tools.catalog import render_catalog
|
|
30
|
+
from adk_code_mode.tools.namespacing import NamespacedTool
|
|
31
|
+
|
|
32
|
+
_OPEN = "<code-mode>"
|
|
33
|
+
_CLOSE = "</code-mode>"
|
|
34
|
+
_INDENT = " "
|
|
35
|
+
|
|
36
|
+
HOW_TO_USE = (
|
|
37
|
+
"This section describes the sandbox that `execute_code` runs in: the Python "
|
|
38
|
+
"version available, the third-party packages preinstalled in it, and the "
|
|
39
|
+
"functions you can import from the `tools` package."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
DISCOVER_TOOLS = "List `/tools/` and read a file's docstring to see what's available."
|
|
43
|
+
|
|
44
|
+
_TOOLS_TOO_LARGE = f"The full catalog is too large to include here. {DISCOVER_TOOLS}"
|
|
45
|
+
|
|
46
|
+
# identity -> (python version, {import name: {distribution name: version}})
|
|
47
|
+
_ENVIRONMENTS: dict[str, tuple[str, dict[str, dict[str, str]]]] = {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def backend_identity(backend: Any) -> str:
|
|
51
|
+
"""Return the cache key for a backend's sandbox image.
|
|
52
|
+
|
|
53
|
+
Built-in backends expose ``identity`` (URL or image tag). Third-party
|
|
54
|
+
backends without one collapse to their class name, which is still stable
|
|
55
|
+
within a process.
|
|
56
|
+
"""
|
|
57
|
+
return getattr(backend, "identity", None) or type(backend).__name__
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def record(identity: str, frame: ReadyFrame) -> None:
|
|
61
|
+
"""Cache the environment a sandbox reported on connect.
|
|
62
|
+
|
|
63
|
+
A frame carrying neither field is ignored rather than stored: sandboxes
|
|
64
|
+
older than this field, and any future reconnect that fails to report, must
|
|
65
|
+
not blank out an environment we already know.
|
|
66
|
+
"""
|
|
67
|
+
if not frame.python_version and not frame.packages:
|
|
68
|
+
return
|
|
69
|
+
packages = {
|
|
70
|
+
import_name: dict(distributions) for import_name, distributions in frame.packages.items()
|
|
71
|
+
}
|
|
72
|
+
_ENVIRONMENTS[identity] = (frame.python_version, packages)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def reset() -> None:
|
|
76
|
+
"""Drop every cached environment. For tests."""
|
|
77
|
+
_ENVIRONMENTS.clear()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _tag(name: str, content: str) -> str:
|
|
81
|
+
# The catalog's sections end in a newline; strip so every tag closes flush
|
|
82
|
+
# against its content rather than after a stray blank line.
|
|
83
|
+
body = content.strip("\n")
|
|
84
|
+
return f"{_INDENT}<{name}>\n{body}\n{_INDENT}</{name}>"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _inline_tag(name: str, value: str) -> str:
|
|
88
|
+
return f"{_INDENT}<{name}>{value}</{name}>"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wrap(children: list[str]) -> str:
|
|
92
|
+
return "\n".join([_OPEN, *children, _CLOSE])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render(*, identity: str, namespaced: list[NamespacedTool], max_chars: int) -> str:
|
|
96
|
+
"""Render the ``<code-mode>`` block, degrading until it fits ``max_chars``.
|
|
97
|
+
|
|
98
|
+
``max_chars`` is measured against the final string, tags included. Tiers,
|
|
99
|
+
in order: the full catalog; import lines only (no signatures or
|
|
100
|
+
docstrings); a pointer telling the model to read ``/tools/`` itself. The
|
|
101
|
+
last tier is unconditional — an oversized tool surface must never cost the
|
|
102
|
+
model the Python version and package list, which are small and which it
|
|
103
|
+
cannot discover any other way.
|
|
104
|
+
|
|
105
|
+
Tags whose content is unknown (no ``ReadyFrame`` yet) or empty (a base
|
|
106
|
+
image with no extra packages, a tool with no tools) are omitted entirely
|
|
107
|
+
rather than rendered blank, which would read as "none available".
|
|
108
|
+
"""
|
|
109
|
+
python_version, packages = _ENVIRONMENTS.get(identity, ("", {}))
|
|
110
|
+
|
|
111
|
+
head = [_tag("how-to-use", HOW_TO_USE)]
|
|
112
|
+
if python_version:
|
|
113
|
+
head.append(_inline_tag("python-version", python_version))
|
|
114
|
+
if packages:
|
|
115
|
+
lines = []
|
|
116
|
+
for import_name, distributions in sorted(
|
|
117
|
+
packages.items(),
|
|
118
|
+
key=lambda item: (item[0].casefold(), item[0]),
|
|
119
|
+
):
|
|
120
|
+
providers = ", ".join(
|
|
121
|
+
f"{name} {version}"
|
|
122
|
+
for name, version in sorted(
|
|
123
|
+
distributions.items(),
|
|
124
|
+
key=lambda item: (item[0].casefold(), item[0]),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
if providers:
|
|
128
|
+
lines.append(f"{import_name}: {providers}")
|
|
129
|
+
if lines:
|
|
130
|
+
head.append(_tag("installed-packages", "\n".join(lines)))
|
|
131
|
+
|
|
132
|
+
if not namespaced:
|
|
133
|
+
return _wrap(head)
|
|
134
|
+
|
|
135
|
+
for detail in ("full", "names"):
|
|
136
|
+
block = _wrap([*head, _tag("tools-package", render_catalog(namespaced, detail=detail))])
|
|
137
|
+
if len(block) <= max_chars:
|
|
138
|
+
return block
|
|
139
|
+
return _wrap([*head, _tag("tools-package", _TOOLS_TOO_LARGE)])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = [
|
|
143
|
+
"DISCOVER_TOOLS",
|
|
144
|
+
"HOW_TO_USE",
|
|
145
|
+
"backend_identity",
|
|
146
|
+
"record",
|
|
147
|
+
"render",
|
|
148
|
+
"reset",
|
|
149
|
+
]
|
|
@@ -88,6 +88,11 @@ class UnsafeLocalDockerBackend:
|
|
|
88
88
|
extra_env: Mapping[str, str] = field(default_factory=dict)
|
|
89
89
|
run_kwargs: Mapping[str, Any] = field(default_factory=dict)
|
|
90
90
|
|
|
91
|
+
@property
|
|
92
|
+
def identity(self) -> str:
|
|
93
|
+
"""Cache key for what this backend's sandbox image contains."""
|
|
94
|
+
return self.image
|
|
95
|
+
|
|
91
96
|
async def start(
|
|
92
97
|
self,
|
|
93
98
|
*,
|
|
@@ -39,10 +39,20 @@ FrameKind = Literal[
|
|
|
39
39
|
|
|
40
40
|
@dataclass(frozen=True)
|
|
41
41
|
class ReadyFrame:
|
|
42
|
-
"""Sandbox → host. Sent once after boot.
|
|
42
|
+
"""Sandbox → host. Sent once after boot.
|
|
43
|
+
|
|
44
|
+
``python_version`` / ``packages`` describe the image the sandbox runs in so
|
|
45
|
+
the host can tell the model what it can import. ``packages`` maps each
|
|
46
|
+
top-level import name to the distributions that provide it and their
|
|
47
|
+
versions. Both are additive fields with defaults, so a host and a sandbox
|
|
48
|
+
on different releases still speak: ``decode`` drops unknown keys, and an
|
|
49
|
+
older sandbox that sends neither just leaves them empty.
|
|
50
|
+
"""
|
|
43
51
|
|
|
44
52
|
kind: Literal["ready"] = "ready"
|
|
45
53
|
protocol_version: int = PROTOCOL_VERSION
|
|
54
|
+
python_version: str = ""
|
|
55
|
+
packages: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
46
56
|
|
|
47
57
|
|
|
48
58
|
@dataclass(frozen=True)
|
|
@@ -77,6 +77,11 @@ class RemoteBackend:
|
|
|
77
77
|
max_upload_workspace_bytes: int = 100 * 1024 * 1024
|
|
78
78
|
max_download_workspace_bytes: int = 100 * 1024 * 1024
|
|
79
79
|
|
|
80
|
+
@property
|
|
81
|
+
def identity(self) -> str:
|
|
82
|
+
"""Cache key for what this backend's sandbox image contains."""
|
|
83
|
+
return self.url
|
|
84
|
+
|
|
80
85
|
async def start(
|
|
81
86
|
self,
|
|
82
87
|
*,
|