adk-code-mode 2.0.1__tar.gz → 2.1.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-2.1.0/.release-please-manifest.json +3 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/CHANGELOG.md +8 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/PKG-INFO +36 -20
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/README.md +35 -19
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/__about__.py +1 -1
- adk_code_mode-2.1.0/src/adk_code_mode/_json.py +43 -0
- adk_code_mode-2.1.0/src/adk_code_mode/mcp_results.py +30 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/metadata.py +34 -13
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool.py +43 -51
- adk_code_mode-2.1.0/src/adk_code_mode/tools/catalog.py +69 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/stubs.py +289 -19
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_catalog.py +48 -1
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_dispatcher.py +55 -17
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_host_loop.py +2 -2
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_metadata.py +8 -3
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_stubs.py +336 -1
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_tool.py +34 -0
- adk_code_mode-2.0.1/.release-please-manifest.json +0 -3
- adk_code_mode-2.0.1/src/adk_code_mode/tools/catalog.py +0 -87
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.githooks/pre-commit +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.githooks/pre-push +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.gitignore +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.prettierrc +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.vscode/settings.json +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.zed/settings.json +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/LICENSE +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/Makefile +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/docker/Dockerfile +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/pyproject.toml +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/release-please-config.json +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_interrupt.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/_artifact_tools.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/output.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/base.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/docker.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/protocol.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/remote.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool_failures.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool_result_artifacts.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/namespacing.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/normaliser.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/workspace/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/workspace/files.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/__init__.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_fake_runtime.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_rest_tool.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/conftest.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_agent_with_fake_llm.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_artifact_tools.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_docker_image.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_docker_integration.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_integration.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_namespacing.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_normaliser.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_output.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_protocol.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_remote_backend.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_remote_integration.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_sandbox_entry.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_stubs_json_schema.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_tool_result_artifacts.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_version.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_workspace_files.py +0 -0
- {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/uv.lock +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0](https://github.com/buildagenta/adk-code-mode/compare/adk-code-mode-v2.0.1...adk-code-mode-v2.1.0) (2026-11-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* show return types in the catalog and point the names tier at stubs ([#29](https://github.com/buildagenta/adk-code-mode/issues/29)) ([a238bfd](https://github.com/buildagenta/adk-code-mode/commit/a238bfd0eecf0acf371fe879a529a85a9e1f2dba))
|
|
9
|
+
* type stub return values and escape docstrings ([#28](https://github.com/buildagenta/adk-code-mode/issues/28)) ([dcb15ab](https://github.com/buildagenta/adk-code-mode/commit/dcb15abfcecc841dc06f5051f41251a1f6ab9100))
|
|
10
|
+
|
|
3
11
|
## [2.0.1](https://github.com/buildagenta/adk-code-mode/compare/adk-code-mode-v2.0.0...adk-code-mode-v2.0.1) (2026-11-02)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.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/buildagenta/adk-code-mode#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/buildagenta/adk-code-mode/issues
|
|
@@ -41,7 +41,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
41
41
|
- **Call ADK tools from sandbox code** — imports against the `tools` package proxy back to the host and run through ADK's `before_tool` / `after_tool` / `on_error` callbacks and the plugin manager exactly as direct tool calls would.
|
|
42
42
|
- **Failed tool calls raise** — a REST tool whose API answers with an HTTP error raises `HTTPStatusError` (with `status_code` and `body`), and any other failure, including an MCP tool that reports one, raises `ToolError`. ADK returns these as values for a model to read, but sandbox code reads return values, so it would otherwise carry on as if the call had worked.
|
|
43
43
|
- **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.
|
|
44
|
-
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
44
|
+
- **Cross-turn persistence via ADK Artifacts** — files the code creates or changes are saved as artifacts automatically, and `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
45
45
|
- **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`.
|
|
46
46
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
47
47
|
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's `execute_code` calls. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
|
|
@@ -252,9 +252,7 @@ save_artifact(
|
|
|
252
252
|
mime_type="application/json",
|
|
253
253
|
)
|
|
254
254
|
print(list_artifacts())
|
|
255
|
-
report = load_artifact(filename="report.json")
|
|
256
|
-
if report is not None and report["kind"] == "text":
|
|
257
|
-
payload = json.loads(report["data"])
|
|
255
|
+
report = json.loads(load_artifact(filename="report.json")["data"])
|
|
258
256
|
```
|
|
259
257
|
|
|
260
258
|
Pass `include_artifact_tools=False` to opt out. To react when the model saves an artifact, pass `on_artifacts_saved`:
|
|
@@ -270,7 +268,7 @@ ExecuteCodeTool(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
|
270
268
|
|
|
271
269
|
### Tool results as artifacts
|
|
272
270
|
|
|
273
|
-
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
|
|
271
|
+
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 lets a later turn reload a result with `load_artifact`. Results over 50,000 bytes return a dictionary with a `tool_result_artifact` filename and a `note` instead of the inline result.
|
|
274
272
|
|
|
275
273
|
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.
|
|
276
274
|
|
|
@@ -338,14 +336,18 @@ Two surfaces, deliberately kept apart. The **tool description** is the invariant
|
|
|
338
336
|
Execute Python code in a sandboxed container. Custom tools are Python functions to
|
|
339
337
|
import from the `tools` package (e.g. `from tools.slack import send_message`). See
|
|
340
338
|
the `<code-mode>` section of the system instruction for the Python version,
|
|
341
|
-
preinstalled packages, and available functions. Print
|
|
342
|
-
stdout/stderr are returned. Variables and
|
|
343
|
-
within the same turn and reset on the next turn
|
|
344
|
-
|
|
345
|
-
|
|
339
|
+
preinstalled packages, and available functions. Print function calls, variables, etc.
|
|
340
|
+
to see output, only stdout/stderr are returned. Variables and files persist across
|
|
341
|
+
calls within the same turn and reset on the next turn. Every file the code writes in
|
|
342
|
+
the working directory, and every tool's result, is saved as an artifact; use
|
|
343
|
+
`load_artifact` (imported the same way) to read one in a later turn. Large tool
|
|
344
|
+
results return a `tool_result_artifact` pointer. Tool calls that
|
|
345
|
+
call APIs raise HTTP errors as `HTTPStatusError` (with `status_code` and `body`);
|
|
346
|
+
other failures raise `ToolError`. Import both from `tools`. Each call can run for up
|
|
347
|
+
to 60s.
|
|
346
348
|
~~~
|
|
347
349
|
|
|
348
|
-
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by
|
|
350
|
+
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by the discovery text the pointer tier uses (see [Fitting the budget](#fitting-the-budget)), so the description never points at a section that wasn't appended. With `save_tool_results_as_artifacts=False` the description drops "and every tool's result" and the sentence about large results, and with `include_artifact_tools=False` it drops the `load_artifact` clause. The last sentence follows `timeout_seconds` and is left out when it's `None`.
|
|
349
351
|
|
|
350
352
|
The **`<code-mode>` block** is the per-deployment inventory of the sandbox, appended to the system instruction on every model turn:
|
|
351
353
|
|
|
@@ -366,7 +368,15 @@ yaml: PyYAML 6.0.2
|
|
|
366
368
|
|
|
367
369
|
from tools.slack import list_channels, send_message
|
|
368
370
|
|
|
369
|
-
|
|
371
|
+
class ListChannelsResponse(TypedDict):
|
|
372
|
+
channels: list[ChannelsItem]
|
|
373
|
+
next_cursor: NotRequired[str]
|
|
374
|
+
|
|
375
|
+
class ChannelsItem(TypedDict):
|
|
376
|
+
id: str
|
|
377
|
+
name: str
|
|
378
|
+
|
|
379
|
+
def list_channels() -> ListChannelsResponse:
|
|
370
380
|
"""List Slack channels."""
|
|
371
381
|
...
|
|
372
382
|
|
|
@@ -382,6 +392,8 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
382
392
|
</code-mode>
|
|
383
393
|
~~~
|
|
384
394
|
|
|
395
|
+
Return types come from each tool's `response_json_schema`, or an MCP tool's `outputSchema`; a tool without one returns `Any`. Objects become `TypedDict` classes named after their `$defs` key or `title`, else the property holding them. Identical shapes share one class across a module, defined once at the top of its catalog section; a name clash with another module's class falls back to the tool's or parent's name; each stub file carries the classes its tool uses. An MCP tool's `outputSchema` describes the `structuredContent` field in its result. A saved tool result can also be a `ToolResultArtifact` pointer when it exceeds the size threshold. A tool with exactly one required parameter takes it positionally (`load_artifact("report.csv", version=2)`); every other parameter is keyword-only.
|
|
396
|
+
|
|
385
397
|
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".
|
|
386
398
|
|
|
387
399
|
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.
|
|
@@ -391,7 +403,10 @@ With `save_tool_results_as_artifacts` enabled (the default), each non-artifact t
|
|
|
391
403
|
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.
|
|
392
404
|
|
|
393
405
|
1. **Full** — as above.
|
|
394
|
-
2. **Names only** — signatures and docstrings dropped, import lines kept:
|
|
406
|
+
2. **Names only** — types, signatures and docstrings dropped, import lines kept. `<how-to-use>` gains:
|
|
407
|
+
|
|
408
|
+
> Before calling a function you haven't used, print its stub to see its signature, docstring and the fields of its return type: `/tools/<module>/<fn>.py`, or `/tools/<fn>.py` for a function imported from `tools`. Print every stub you need in one `execute_code` call.
|
|
409
|
+
|
|
395
410
|
|
|
396
411
|
~~~
|
|
397
412
|
<tools-package>
|
|
@@ -401,11 +416,11 @@ A block over `max_code_mode_metadata_chars` degrades in tiers instead of vanishi
|
|
|
401
416
|
</tools-package>
|
|
402
417
|
~~~
|
|
403
418
|
|
|
404
|
-
3. **Pointer** — the catalog is replaced by a
|
|
419
|
+
3. **Pointer** — the catalog is replaced by a note, and this tier is emitted no matter how small the budget. `<how-to-use>` gains the same text, prefixed with "List `/tools/` and its module directories to find the functions available.":
|
|
405
420
|
|
|
406
421
|
~~~
|
|
407
422
|
<tools-package>
|
|
408
|
-
The full catalog is too large to include here.
|
|
423
|
+
The full catalog is too large to include here.
|
|
409
424
|
</tools-package>
|
|
410
425
|
~~~
|
|
411
426
|
|
|
@@ -413,11 +428,12 @@ At the last tier the model navigates the sandbox from Python instead:
|
|
|
413
428
|
|
|
414
429
|
```python
|
|
415
430
|
import pathlib
|
|
416
|
-
print(
|
|
417
|
-
print(open("/tools/slack/send_message.py").read()) # signature + docstring
|
|
431
|
+
print(sorted(pathlib.Path("/tools").rglob("*.py")))
|
|
432
|
+
print(open("/tools/slack/send_message.py").read()) # types, signature + docstring
|
|
433
|
+
print(open("/tools/slack/list_channels.py").read())
|
|
418
434
|
```
|
|
419
435
|
|
|
420
|
-
Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns `
|
|
436
|
+
Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns a dictionary with `kind`, `data`, and `mime_type`. Text and JSON use `kind="text"` and keep the string in `data`. Binary artifacts use `kind="bytes"` and store base64 in `data`.
|
|
421
437
|
|
|
422
438
|
## 🛡️ Safety
|
|
423
439
|
|
|
@@ -450,7 +466,7 @@ Named "Unsafe" intentionally: it binds a TCP listener on `0.0.0.0`, communicates
|
|
|
450
466
|
## ⚠️ Limitations
|
|
451
467
|
|
|
452
468
|
- **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.
|
|
453
|
-
- **State is turn-scoped.** Variables and the working directory persist across `execute_code` calls **within** a turn, but reset between turns.
|
|
469
|
+
- **State is turn-scoped.** Variables and the working directory persist across `execute_code` calls **within** a turn, but reset between turns. Files the code writes and tool results are saved as artifacts; use `load_artifact` to read them in a later turn.
|
|
454
470
|
- **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.
|
|
455
471
|
|
|
456
472
|
## 🛠️ Development
|
|
@@ -14,7 +14,7 @@ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and
|
|
|
14
14
|
- **Call ADK tools from sandbox code** — imports against the `tools` package proxy back to the host and run through ADK's `before_tool` / `after_tool` / `on_error` callbacks and the plugin manager exactly as direct tool calls would.
|
|
15
15
|
- **Failed tool calls raise** — a REST tool whose API answers with an HTTP error raises `HTTPStatusError` (with `status_code` and `body`), and any other failure, including an MCP tool that reports one, raises `ToolError`. ADK returns these as values for a model to read, but sandbox code reads return values, so it would otherwise carry on as if the call had worked.
|
|
16
16
|
- **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.
|
|
17
|
-
- **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
17
|
+
- **Cross-turn persistence via ADK Artifacts** — files the code creates or changes are saved as artifacts automatically, and `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
|
|
18
18
|
- **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`.
|
|
19
19
|
- **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
|
|
20
20
|
- **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated per-turn container over WebSocket, reused across the turn's `execute_code` calls. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
|
|
@@ -225,9 +225,7 @@ save_artifact(
|
|
|
225
225
|
mime_type="application/json",
|
|
226
226
|
)
|
|
227
227
|
print(list_artifacts())
|
|
228
|
-
report = load_artifact(filename="report.json")
|
|
229
|
-
if report is not None and report["kind"] == "text":
|
|
230
|
-
payload = json.loads(report["data"])
|
|
228
|
+
report = json.loads(load_artifact(filename="report.json")["data"])
|
|
231
229
|
```
|
|
232
230
|
|
|
233
231
|
Pass `include_artifact_tools=False` to opt out. To react when the model saves an artifact, pass `on_artifacts_saved`:
|
|
@@ -243,7 +241,7 @@ ExecuteCodeTool(tools=..., backend=..., on_artifacts_saved=on_saved)
|
|
|
243
241
|
|
|
244
242
|
### Tool results as artifacts
|
|
245
243
|
|
|
246
|
-
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
|
|
244
|
+
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 lets a later turn reload a result with `load_artifact`. Results over 50,000 bytes return a dictionary with a `tool_result_artifact` filename and a `note` instead of the inline result.
|
|
247
245
|
|
|
248
246
|
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.
|
|
249
247
|
|
|
@@ -311,14 +309,18 @@ Two surfaces, deliberately kept apart. The **tool description** is the invariant
|
|
|
311
309
|
Execute Python code in a sandboxed container. Custom tools are Python functions to
|
|
312
310
|
import from the `tools` package (e.g. `from tools.slack import send_message`). See
|
|
313
311
|
the `<code-mode>` section of the system instruction for the Python version,
|
|
314
|
-
preinstalled packages, and available functions. Print
|
|
315
|
-
stdout/stderr are returned. Variables and
|
|
316
|
-
within the same turn and reset on the next turn
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
preinstalled packages, and available functions. Print function calls, variables, etc.
|
|
313
|
+
to see output, only stdout/stderr are returned. Variables and files persist across
|
|
314
|
+
calls within the same turn and reset on the next turn. Every file the code writes in
|
|
315
|
+
the working directory, and every tool's result, is saved as an artifact; use
|
|
316
|
+
`load_artifact` (imported the same way) to read one in a later turn. Large tool
|
|
317
|
+
results return a `tool_result_artifact` pointer. Tool calls that
|
|
318
|
+
call APIs raise HTTP errors as `HTTPStatusError` (with `status_code` and `body`);
|
|
319
|
+
other failures raise `ToolError`. Import both from `tools`. Each call can run for up
|
|
320
|
+
to 60s.
|
|
319
321
|
~~~
|
|
320
322
|
|
|
321
|
-
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by
|
|
323
|
+
With `append_code_mode_metadata_to_system_instruction` disabled that middle sentence is replaced by the discovery text the pointer tier uses (see [Fitting the budget](#fitting-the-budget)), so the description never points at a section that wasn't appended. With `save_tool_results_as_artifacts=False` the description drops "and every tool's result" and the sentence about large results, and with `include_artifact_tools=False` it drops the `load_artifact` clause. The last sentence follows `timeout_seconds` and is left out when it's `None`.
|
|
322
324
|
|
|
323
325
|
The **`<code-mode>` block** is the per-deployment inventory of the sandbox, appended to the system instruction on every model turn:
|
|
324
326
|
|
|
@@ -339,7 +341,15 @@ yaml: PyYAML 6.0.2
|
|
|
339
341
|
|
|
340
342
|
from tools.slack import list_channels, send_message
|
|
341
343
|
|
|
342
|
-
|
|
344
|
+
class ListChannelsResponse(TypedDict):
|
|
345
|
+
channels: list[ChannelsItem]
|
|
346
|
+
next_cursor: NotRequired[str]
|
|
347
|
+
|
|
348
|
+
class ChannelsItem(TypedDict):
|
|
349
|
+
id: str
|
|
350
|
+
name: str
|
|
351
|
+
|
|
352
|
+
def list_channels() -> ListChannelsResponse:
|
|
343
353
|
"""List Slack channels."""
|
|
344
354
|
...
|
|
345
355
|
|
|
@@ -355,6 +365,8 @@ from tools import save_artifact, load_artifact, list_artifacts
|
|
|
355
365
|
</code-mode>
|
|
356
366
|
~~~
|
|
357
367
|
|
|
368
|
+
Return types come from each tool's `response_json_schema`, or an MCP tool's `outputSchema`; a tool without one returns `Any`. Objects become `TypedDict` classes named after their `$defs` key or `title`, else the property holding them. Identical shapes share one class across a module, defined once at the top of its catalog section; a name clash with another module's class falls back to the tool's or parent's name; each stub file carries the classes its tool uses. An MCP tool's `outputSchema` describes the `structuredContent` field in its result. A saved tool result can also be a `ToolResultArtifact` pointer when it exceeds the size threshold. A tool with exactly one required parameter takes it positionally (`load_artifact("report.csv", version=2)`); every other parameter is keyword-only.
|
|
369
|
+
|
|
358
370
|
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".
|
|
359
371
|
|
|
360
372
|
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.
|
|
@@ -364,7 +376,10 @@ With `save_tool_results_as_artifacts` enabled (the default), each non-artifact t
|
|
|
364
376
|
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.
|
|
365
377
|
|
|
366
378
|
1. **Full** — as above.
|
|
367
|
-
2. **Names only** — signatures and docstrings dropped, import lines kept:
|
|
379
|
+
2. **Names only** — types, signatures and docstrings dropped, import lines kept. `<how-to-use>` gains:
|
|
380
|
+
|
|
381
|
+
> Before calling a function you haven't used, print its stub to see its signature, docstring and the fields of its return type: `/tools/<module>/<fn>.py`, or `/tools/<fn>.py` for a function imported from `tools`. Print every stub you need in one `execute_code` call.
|
|
382
|
+
|
|
368
383
|
|
|
369
384
|
~~~
|
|
370
385
|
<tools-package>
|
|
@@ -374,11 +389,11 @@ A block over `max_code_mode_metadata_chars` degrades in tiers instead of vanishi
|
|
|
374
389
|
</tools-package>
|
|
375
390
|
~~~
|
|
376
391
|
|
|
377
|
-
3. **Pointer** — the catalog is replaced by a
|
|
392
|
+
3. **Pointer** — the catalog is replaced by a note, and this tier is emitted no matter how small the budget. `<how-to-use>` gains the same text, prefixed with "List `/tools/` and its module directories to find the functions available.":
|
|
378
393
|
|
|
379
394
|
~~~
|
|
380
395
|
<tools-package>
|
|
381
|
-
The full catalog is too large to include here.
|
|
396
|
+
The full catalog is too large to include here.
|
|
382
397
|
</tools-package>
|
|
383
398
|
~~~
|
|
384
399
|
|
|
@@ -386,11 +401,12 @@ At the last tier the model navigates the sandbox from Python instead:
|
|
|
386
401
|
|
|
387
402
|
```python
|
|
388
403
|
import pathlib
|
|
389
|
-
print(
|
|
390
|
-
print(open("/tools/slack/send_message.py").read()) # signature + docstring
|
|
404
|
+
print(sorted(pathlib.Path("/tools").rglob("*.py")))
|
|
405
|
+
print(open("/tools/slack/send_message.py").read()) # types, signature + docstring
|
|
406
|
+
print(open("/tools/slack/list_channels.py").read())
|
|
391
407
|
```
|
|
392
408
|
|
|
393
|
-
Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns `
|
|
409
|
+
Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns a dictionary with `kind`, `data`, and `mime_type`. Text and JSON use `kind="text"` and keep the string in `data`. Binary artifacts use `kind="bytes"` and store base64 in `data`.
|
|
394
410
|
|
|
395
411
|
## 🛡️ Safety
|
|
396
412
|
|
|
@@ -423,7 +439,7 @@ Named "Unsafe" intentionally: it binds a TCP listener on `0.0.0.0`, communicates
|
|
|
423
439
|
## ⚠️ Limitations
|
|
424
440
|
|
|
425
441
|
- **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.
|
|
426
|
-
- **State is turn-scoped.** Variables and the working directory persist across `execute_code` calls **within** a turn, but reset between turns.
|
|
442
|
+
- **State is turn-scoped.** Variables and the working directory persist across `execute_code` calls **within** a turn, but reset between turns. Files the code writes and tool results are saved as artifacts; use `load_artifact` to read them in a later turn.
|
|
427
443
|
- **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.
|
|
428
444
|
|
|
429
445
|
## 🛠️ Development
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present Agenta <hello@buildagenta.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""JSON-safe conversion for the sandbox RPC reply."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import dataclasses
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def json_safe(value: Any) -> Any:
|
|
17
|
+
"""Best-effort conversion of a tool result to a JSON-safe value."""
|
|
18
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
19
|
+
return value
|
|
20
|
+
if isinstance(value, (bytes, bytearray)):
|
|
21
|
+
return {
|
|
22
|
+
"kind": "bytes",
|
|
23
|
+
"data": base64.b64encode(bytes(value)).decode("ascii"),
|
|
24
|
+
"encoding": "base64",
|
|
25
|
+
}
|
|
26
|
+
if isinstance(value, (dt.datetime, dt.date)):
|
|
27
|
+
return value.isoformat()
|
|
28
|
+
if isinstance(value, Decimal):
|
|
29
|
+
return str(value)
|
|
30
|
+
if isinstance(value, Path):
|
|
31
|
+
return str(value)
|
|
32
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
33
|
+
return [json_safe(v) for v in value]
|
|
34
|
+
if isinstance(value, dict):
|
|
35
|
+
return {str(k): json_safe(v) for k, v in value.items()}
|
|
36
|
+
if hasattr(value, "model_dump"):
|
|
37
|
+
try:
|
|
38
|
+
return json_safe(value.model_dump(mode="json"))
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
42
|
+
return json_safe(dataclasses.asdict(value))
|
|
43
|
+
return repr(value)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present Agenta <hello@buildagenta.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Describe the MCP result envelope returned by ADK."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from google.adk.tools.base_tool import BaseTool
|
|
11
|
+
from google.adk.tools.mcp_tool.mcp_tool import McpTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def response_schema(tool: BaseTool) -> dict[str, Any] | None:
|
|
15
|
+
"""Describe an MCP result and type its structured content from the tool's output schema."""
|
|
16
|
+
if not isinstance(tool, McpTool) or not (output := tool.raw_mcp_tool.outputSchema):
|
|
17
|
+
return None
|
|
18
|
+
return {
|
|
19
|
+
**{key: output[key] for key in ("$defs", "definitions") if key in output},
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"content": {"type": "array", "items": {}},
|
|
23
|
+
"isError": {"type": "boolean"},
|
|
24
|
+
"structuredContent": {"anyOf": [output, {"type": "null"}]},
|
|
25
|
+
},
|
|
26
|
+
"required": ["content", "isError"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["response_schema"]
|
|
@@ -26,7 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
from typing import Any
|
|
27
27
|
|
|
28
28
|
from adk_code_mode.runtime.protocol import ReadyFrame
|
|
29
|
-
from adk_code_mode.tools.catalog import render_catalog
|
|
29
|
+
from adk_code_mode.tools.catalog import Detail, render_catalog
|
|
30
30
|
from adk_code_mode.tools.namespacing import NamespacedTool
|
|
31
31
|
|
|
32
32
|
_OPEN = "<code-mode>"
|
|
@@ -39,9 +39,20 @@ HOW_TO_USE = (
|
|
|
39
39
|
"functions you can import from the `tools` package."
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
# Every tier without signatures needs these: the full tier shows them inline.
|
|
43
|
+
READ_BEFORE_CALLING = (
|
|
44
|
+
"Before calling a function you haven't used, print its stub to see its "
|
|
45
|
+
"signature, docstring and the fields of its return type: "
|
|
46
|
+
"`/tools/<module>/<fn>.py`, or `/tools/<fn>.py` for a function imported from "
|
|
47
|
+
"`tools`. Print every stub you need in one `execute_code` call."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
DISCOVER_TOOLS = (
|
|
51
|
+
f"List `/tools/` and its module directories to find the functions available. "
|
|
52
|
+
f"{READ_BEFORE_CALLING}"
|
|
53
|
+
)
|
|
43
54
|
|
|
44
|
-
_TOOLS_TOO_LARGE =
|
|
55
|
+
_TOOLS_TOO_LARGE = "The full catalog is too large to include here."
|
|
45
56
|
|
|
46
57
|
# identity -> (python version, {import name: {distribution name: version}})
|
|
47
58
|
_ENVIRONMENTS: dict[str, tuple[str, dict[str, dict[str, str]]]] = {}
|
|
@@ -96,8 +107,10 @@ def render(*, identity: str, namespaced: list[NamespacedTool], max_chars: int) -
|
|
|
96
107
|
"""Render the ``<code-mode>`` block, degrading until it fits ``max_chars``.
|
|
97
108
|
|
|
98
109
|
``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)
|
|
110
|
+
in order: the full catalog; import lines only (no types, signatures or
|
|
111
|
+
docstrings), with ``READ_BEFORE_CALLING`` added to the how-to-use text; a
|
|
112
|
+
pointer, with ``DISCOVER_TOOLS`` added instead, telling the model to read
|
|
113
|
+
``/tools/`` itself. The
|
|
101
114
|
last tier is unconditional — an oversized tool surface must never cost the
|
|
102
115
|
model the Python version and package list, which are small and which it
|
|
103
116
|
cannot discover any other way.
|
|
@@ -108,9 +121,9 @@ def render(*, identity: str, namespaced: list[NamespacedTool], max_chars: int) -
|
|
|
108
121
|
"""
|
|
109
122
|
python_version, packages = _ENVIRONMENTS.get(identity, ("", {}))
|
|
110
123
|
|
|
111
|
-
|
|
124
|
+
environment: list[str] = []
|
|
112
125
|
if python_version:
|
|
113
|
-
|
|
126
|
+
environment.append(_inline_tag("python-version", python_version))
|
|
114
127
|
if packages:
|
|
115
128
|
lines = []
|
|
116
129
|
for import_name, distributions in sorted(
|
|
@@ -127,21 +140,29 @@ def render(*, identity: str, namespaced: list[NamespacedTool], max_chars: int) -
|
|
|
127
140
|
if providers:
|
|
128
141
|
lines.append(f"{import_name}: {providers}")
|
|
129
142
|
if lines:
|
|
130
|
-
|
|
143
|
+
environment.append(_tag("installed-packages", "\n".join(lines)))
|
|
131
144
|
|
|
145
|
+
how_to_use = _tag("how-to-use", HOW_TO_USE)
|
|
132
146
|
if not namespaced:
|
|
133
|
-
return _wrap(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
return _wrap([how_to_use, *environment])
|
|
148
|
+
|
|
149
|
+
tiers: list[tuple[Detail, str]] = [
|
|
150
|
+
("full", how_to_use),
|
|
151
|
+
("names", _tag("how-to-use", f"{HOW_TO_USE} {READ_BEFORE_CALLING}")),
|
|
152
|
+
]
|
|
153
|
+
for detail, tier_how_to_use in tiers:
|
|
154
|
+
catalog = _tag("tools-package", render_catalog(namespaced, detail=detail))
|
|
155
|
+
block = _wrap([tier_how_to_use, *environment, catalog])
|
|
137
156
|
if len(block) <= max_chars:
|
|
138
157
|
return block
|
|
139
|
-
|
|
158
|
+
discover = _tag("how-to-use", f"{HOW_TO_USE} {DISCOVER_TOOLS}")
|
|
159
|
+
return _wrap([discover, *environment, _tag("tools-package", _TOOLS_TOO_LARGE)])
|
|
140
160
|
|
|
141
161
|
|
|
142
162
|
__all__ = [
|
|
143
163
|
"DISCOVER_TOOLS",
|
|
144
164
|
"HOW_TO_USE",
|
|
165
|
+
"READ_BEFORE_CALLING",
|
|
145
166
|
"backend_identity",
|
|
146
167
|
"record",
|
|
147
168
|
"render",
|