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.
Files changed (84) hide show
  1. adk_code_mode-2.1.0/.release-please-manifest.json +3 -0
  2. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/CHANGELOG.md +8 -0
  3. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/PKG-INFO +36 -20
  4. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/README.md +35 -19
  5. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
  6. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/__about__.py +1 -1
  7. adk_code_mode-2.1.0/src/adk_code_mode/_json.py +43 -0
  8. adk_code_mode-2.1.0/src/adk_code_mode/mcp_results.py +30 -0
  9. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/metadata.py +34 -13
  10. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool.py +43 -51
  11. adk_code_mode-2.1.0/src/adk_code_mode/tools/catalog.py +69 -0
  12. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/stubs.py +289 -19
  13. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_catalog.py +48 -1
  14. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_dispatcher.py +55 -17
  15. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_host_loop.py +2 -2
  16. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_metadata.py +8 -3
  17. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_stubs.py +336 -1
  18. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_tool.py +34 -0
  19. adk_code_mode-2.0.1/.release-please-manifest.json +0 -3
  20. adk_code_mode-2.0.1/src/adk_code_mode/tools/catalog.py +0 -87
  21. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.githooks/pre-commit +0 -0
  22. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.githooks/pre-push +0 -0
  23. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.github/workflows/ci.yml +0 -0
  24. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.github/workflows/release-please.yml +0 -0
  25. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.gitignore +0 -0
  26. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.prettierrc +0 -0
  27. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.vscode/settings.json +0 -0
  28. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/.zed/settings.json +0 -0
  29. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/LICENSE +0 -0
  30. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/Makefile +0 -0
  31. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/docker/Dockerfile +0 -0
  32. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/pyproject.toml +0 -0
  33. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/release-please-config.json +0 -0
  34. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/README.md +0 -0
  35. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/pyproject.toml +0 -0
  36. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
  37. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
  38. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
  39. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +0 -0
  40. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_interrupt.py +0 -0
  41. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
  42. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
  43. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/scripts/install-git-hooks.sh +0 -0
  44. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/__init__.py +0 -0
  45. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/_artifact_tools.py +0 -0
  46. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/output.py +0 -0
  47. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/py.typed +0 -0
  48. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/__init__.py +0 -0
  49. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/base.py +0 -0
  50. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/docker.py +0 -0
  51. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/protocol.py +0 -0
  52. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/runtime/remote.py +0 -0
  53. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool_failures.py +0 -0
  54. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tool_result_artifacts.py +0 -0
  55. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/__init__.py +0 -0
  56. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
  57. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/namespacing.py +0 -0
  58. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/tools/normaliser.py +0 -0
  59. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/workspace/__init__.py +0 -0
  60. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/src/adk_code_mode/workspace/files.py +0 -0
  61. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/__init__.py +0 -0
  62. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_docker_helpers.py +0 -0
  63. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_fake_runtime.py +0 -0
  64. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/_rest_tool.py +0 -0
  65. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/conftest.py +0 -0
  66. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_agent_with_fake_llm.py +0 -0
  67. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_artifact_tools.py +0 -0
  68. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_docker_image.py +0 -0
  69. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_docker_integration.py +0 -0
  70. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_integration.py +0 -0
  71. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_namespacing.py +0 -0
  72. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_normaliser.py +0 -0
  73. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_output.py +0 -0
  74. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_protocol.py +0 -0
  75. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_remote_backend.py +0 -0
  76. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_remote_integration.py +0 -0
  77. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_runtime_docker.py +0 -0
  78. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_sandbox_entry.py +0 -0
  79. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_sandbox_rpc_client.py +0 -0
  80. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_stubs_json_schema.py +0 -0
  81. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_tool_result_artifacts.py +0 -0
  82. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_version.py +0 -0
  83. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/tests/test_workspace_files.py +0 -0
  84. {adk_code_mode-2.0.1 → adk_code_mode-2.1.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.1.0"
3
+ }
@@ -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.1
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`. Files the code creates or changes are saved as artifacts automatically too.
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 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`.
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 anything you want to see; only
342
- stdout/stderr are returned. Variables and the working directory persist across calls
343
- within the same turn and reset on the next turn — use `save_artifact` /
344
- `load_artifact` (imported the same way) to persist across turns. Files created or
345
- changed by the code are saved as artifacts automatically
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 `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.
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
- def list_channels() -> Any:
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 discovery note, and this tier is emitted no matter how small the budget:
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. List `/tools/` and read a file's docstring to see what's available.
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(list(pathlib.Path("/tools").iterdir()))
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 `{"kind": "text" | "bytes", "data": str, "mime_type": str | None}`.
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. Use `save_artifact` / `load_artifact` to persist across 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`. Files the code creates or changes are saved as artifacts automatically too.
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 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`.
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 anything you want to see; only
315
- stdout/stderr are returned. Variables and the working directory persist across calls
316
- within the same turn and reset on the next turn — use `save_artifact` /
317
- `load_artifact` (imported the same way) to persist across turns. Files created or
318
- changed by the code are saved as artifacts automatically
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 `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.
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
- def list_channels() -> Any:
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 discovery note, and this tier is emitted no matter how small the budget:
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. List `/tools/` and read a file's docstring to see what's available.
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(list(pathlib.Path("/tools").iterdir()))
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 `{"kind": "text" | "bytes", "data": str, "mime_type": str | None}`.
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. Use `save_artifact` / `load_artifact` to persist across 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
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Agenta <hello@buildagenta.com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "2.0.1" # x-release-please-version
4
+ __version__ = "2.1.0" # x-release-please-version
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Agenta <hello@buildagenta.com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "2.0.1" # x-release-please-version
4
+ __version__ = "2.1.0" # x-release-please-version
@@ -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
- DISCOVER_TOOLS = "List `/tools/` and read a file's docstring to see what's available."
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 = f"The full catalog is too large to include here. {DISCOVER_TOOLS}"
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); a pointer telling the model to read ``/tools/`` itself. The
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
- head = [_tag("how-to-use", HOW_TO_USE)]
124
+ environment: list[str] = []
112
125
  if python_version:
113
- head.append(_inline_tag("python-version", python_version))
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
- head.append(_tag("installed-packages", "\n".join(lines)))
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(head)
134
-
135
- for detail in ("full", "names"):
136
- block = _wrap([*head, _tag("tools-package", render_catalog(namespaced, detail=detail))])
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
- return _wrap([*head, _tag("tools-package", _TOOLS_TOO_LARGE)])
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",