adk-code-mode 1.4.0__tar.gz → 1.6.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.6.0/.release-please-manifest.json +3 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/CHANGELOG.md +15 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/PKG-INFO +13 -37
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/README.md +11 -35
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/__about__.py +1 -1
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tool.py +9 -2
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/namespacing.py +20 -23
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/stubs.py +173 -17
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_catalog.py +12 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_integration.py +10 -1
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_namespacing.py +48 -2
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_stubs_json_schema.py +231 -0
- adk_code_mode-1.4.0/.release-please-manifest.json +0 -3
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.githooks/pre-commit +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.githooks/pre-push +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.github/workflows/ci.yml +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.github/workflows/release-please.yml +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.gitignore +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.prettierrc +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.vscode/settings.json +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/.zed/settings.json +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/LICENSE +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/Makefile +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/docker/Dockerfile +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/pyproject.toml +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/release-please-config.json +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/README.md +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/pyproject.toml +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/scripts/install-git-hooks.sh +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/_artifact_tools.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/metadata.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/output.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/py.typed +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/runtime/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/runtime/base.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/runtime/docker.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/runtime/protocol.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/runtime/remote.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tool_result_artifacts.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/catalog.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/tools/normaliser.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/workspace/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/src/adk_code_mode/workspace/files.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/__init__.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/_docker_helpers.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/_fake_runtime.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/conftest.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_agent_with_fake_llm.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_artifact_tools.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_dispatcher.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_docker_image.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_docker_integration.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_host_loop.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_metadata.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_normaliser.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_output.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_protocol.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_remote_backend.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_remote_integration.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_runtime_docker.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_sandbox_entry.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_sandbox_rpc_client.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_stubs.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_tool.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_tool_result_artifacts.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_version.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/tests/test_workspace_files.py +0 -0
- {adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/uv.lock +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.5.0...adk-code-mode-v1.6.0) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* bound an execute_code block by default ([1609c0d](https://github.com/a2anet/adk-code-mode/commit/1609c0d24b8563cba51434cd1214b4299aeefc86))
|
|
9
|
+
* expand object oneOf/anyOf branches in tool stub docstrings ([#20](https://github.com/a2anet/adk-code-mode/issues/20)) ([7d42cb1](https://github.com/a2anet/adk-code-mode/commit/7d42cb1ee9d9e217d008776dc74a84034f4b67a9))
|
|
10
|
+
|
|
11
|
+
## [1.5.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.4.0...adk-code-mode-v1.5.0) (2026-08-10)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* scope tool-name uniqueness to each namespace ([#17](https://github.com/a2anet/adk-code-mode/issues/17)) ([437bc0d](https://github.com/a2anet/adk-code-mode/commit/437bc0dc930521a933e5c25b4bd6103e9434fc27))
|
|
17
|
+
|
|
3
18
|
## [1.4.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v1.3.0...adk-code-mode-v1.4.0) (2026-07-28)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: adk-code-mode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -85,13 +85,17 @@ Build an `ExecuteCodeTool`, give it the tools the sandboxed code may call, and a
|
|
|
85
85
|
|
|
86
86
|
```python
|
|
87
87
|
from google.adk.agents import LlmAgent
|
|
88
|
+
from google.adk.features import FeatureName, override_feature_enabled
|
|
88
89
|
from adk_code_mode import ExecuteCodeTool, RemoteBackend
|
|
89
90
|
|
|
91
|
+
# Required for docstrings to show full tool schemas
|
|
92
|
+
override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
93
|
+
|
|
90
94
|
tool = ExecuteCodeTool(
|
|
91
95
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
92
96
|
backend=RemoteBackend(
|
|
93
|
-
url="https://sandbox-xyz.run.app",
|
|
94
|
-
token="your-secret-token",
|
|
97
|
+
url="https://sandbox-xyz.run.app",
|
|
98
|
+
token="your-secret-token",
|
|
95
99
|
),
|
|
96
100
|
)
|
|
97
101
|
|
|
@@ -112,8 +116,12 @@ root_agent = LlmAgent(
|
|
|
112
116
|
> **`UnsafeLocalDockerBackend` is not safe for production or multi-tenant use.** See [Safety](#-safety).
|
|
113
117
|
|
|
114
118
|
```python
|
|
119
|
+
from google.adk.features import FeatureName, override_feature_enabled
|
|
115
120
|
from adk_code_mode import ExecuteCodeTool, UnsafeLocalDockerBackend
|
|
116
121
|
|
|
122
|
+
# Required for docstrings to show full tool schemas
|
|
123
|
+
override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
124
|
+
|
|
117
125
|
tool = ExecuteCodeTool(
|
|
118
126
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
119
127
|
backend=UnsafeLocalDockerBackend(image="ghcr.io/a2anet/adk-code-mode:latest"),
|
|
@@ -292,7 +300,7 @@ All settings are `ExecuteCodeTool` constructor arguments:
|
|
|
292
300
|
| `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). |
|
|
293
301
|
| `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. |
|
|
294
302
|
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
295
|
-
| `timeout_seconds` | `
|
|
303
|
+
| `timeout_seconds` | `60` | Caps overall execution time of one `execute_code` call, and with it every tool the block calls. `None` lifts the cap, leaving the platform request timeout (e.g. Cloud Run `--timeout`) as the only bound. |
|
|
296
304
|
| `per_tool_timeout_seconds` | `None` | Caps each individual tool call made from within the sandbox. |
|
|
297
305
|
| `session_idle_timeout_seconds` | `600` | Idle reaper: closes a turn's container once it goes untouched this long. Backstop for turns that never call `release_invocation`. |
|
|
298
306
|
|
|
@@ -377,38 +385,6 @@ Tags are indented; their content is not. The `<tools-package>` body is Python so
|
|
|
377
385
|
|
|
378
386
|
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.
|
|
379
387
|
|
|
380
|
-
#### Schema fidelity
|
|
381
|
-
|
|
382
|
-
Stubs are rendered from each tool's declaration, so whatever the declaration drops can never reach the model. Nested object properties, their descriptions and JSON Schema's validation keywords all land in the docstring, because no Python type expresses them:
|
|
383
|
-
|
|
384
|
-
~~~python
|
|
385
|
-
def create_minimum_spend_rule(*, space_id: int, conditions: dict[str, Any] | None = ..., label: str | None = ...) -> Any:
|
|
386
|
-
"""Creates a new minimum spend rule under the MinimumSpend policy.
|
|
387
|
-
|
|
388
|
-
Args:
|
|
389
|
-
space_id: The unique identifier of the venue space. (format=int32)
|
|
390
|
-
conditions: Conditions that must all be satisfied for the rule to apply (AND logic).
|
|
391
|
-
daysOfWeek: list[int] | None - Days of the week (1=Monday, 7=Sunday). (items: minimum=1, maximum=7)
|
|
392
|
-
timeRange: dict[str, Any] | None - Time window of the booking start time.
|
|
393
|
-
from: str (pattern=^([01][0-9]|2[0-3]):[0-5][0-9]$, example=18:00)
|
|
394
|
-
to: str (example=23:59)
|
|
395
|
-
label: (maxLength=255)
|
|
396
|
-
"""
|
|
397
|
-
~~~
|
|
398
|
-
|
|
399
|
-
Every JSON Schema 2020-12 Validation and Meta-Data keyword is covered; `tests/test_stubs_json_schema.py` reads the keyword set from the meta-schemas and fails if one goes unclassified.
|
|
400
|
-
|
|
401
|
-
> [!IMPORTANT]
|
|
402
|
-
> **Enable ADK's `JSON_SCHEMA_FOR_FUNC_DECL` feature to get the full picture.** It is off by default, and without it ADK builds declarations through a Gemini `types.Schema`, which reduces an `allOf`-wrapped `$ref` — how OpenAPI 3.0 spells a nullable reference — to a bare `{"type": "OBJECT"}`. The referenced properties and description are gone before this package sees them, and the field renders as an empty `dict[str, Any]`.
|
|
403
|
-
>
|
|
404
|
-
> ```python
|
|
405
|
-
> from google.adk.features import FeatureName, override_feature_enabled
|
|
406
|
-
>
|
|
407
|
-
> override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
408
|
-
> ```
|
|
409
|
-
>
|
|
410
|
-
> Or set `ADK_ENABLE_JSON_SCHEMA_FOR_FUNC_DECL=1` in the environment. Do this before building any tool. It changes how declarations are built for every tool in the process, so if the same process also exposes tools to the model directly, check the model layer accepts `parameters_json_schema` — ADK's LiteLLM integration does, and `ExecuteCodeTool` declares itself that way regardless.
|
|
411
|
-
|
|
412
388
|
#### Fitting the budget
|
|
413
389
|
|
|
414
390
|
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.
|
|
@@ -58,13 +58,17 @@ Build an `ExecuteCodeTool`, give it the tools the sandboxed code may call, and a
|
|
|
58
58
|
|
|
59
59
|
```python
|
|
60
60
|
from google.adk.agents import LlmAgent
|
|
61
|
+
from google.adk.features import FeatureName, override_feature_enabled
|
|
61
62
|
from adk_code_mode import ExecuteCodeTool, RemoteBackend
|
|
62
63
|
|
|
64
|
+
# Required for docstrings to show full tool schemas
|
|
65
|
+
override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
66
|
+
|
|
63
67
|
tool = ExecuteCodeTool(
|
|
64
68
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
65
69
|
backend=RemoteBackend(
|
|
66
|
-
url="https://sandbox-xyz.run.app",
|
|
67
|
-
token="your-secret-token",
|
|
70
|
+
url="https://sandbox-xyz.run.app",
|
|
71
|
+
token="your-secret-token",
|
|
68
72
|
),
|
|
69
73
|
)
|
|
70
74
|
|
|
@@ -85,8 +89,12 @@ root_agent = LlmAgent(
|
|
|
85
89
|
> **`UnsafeLocalDockerBackend` is not safe for production or multi-tenant use.** See [Safety](#-safety).
|
|
86
90
|
|
|
87
91
|
```python
|
|
92
|
+
from google.adk.features import FeatureName, override_feature_enabled
|
|
88
93
|
from adk_code_mode import ExecuteCodeTool, UnsafeLocalDockerBackend
|
|
89
94
|
|
|
95
|
+
# Required for docstrings to show full tool schemas
|
|
96
|
+
override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
97
|
+
|
|
90
98
|
tool = ExecuteCodeTool(
|
|
91
99
|
tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
|
|
92
100
|
backend=UnsafeLocalDockerBackend(image="ghcr.io/a2anet/adk-code-mode:latest"),
|
|
@@ -265,7 +273,7 @@ All settings are `ExecuteCodeTool` constructor arguments:
|
|
|
265
273
|
| `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). |
|
|
266
274
|
| `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. |
|
|
267
275
|
| `max_code_chars` | `1_000_000` | Rejects oversized code payloads before starting a container. |
|
|
268
|
-
| `timeout_seconds` | `
|
|
276
|
+
| `timeout_seconds` | `60` | Caps overall execution time of one `execute_code` call, and with it every tool the block calls. `None` lifts the cap, leaving the platform request timeout (e.g. Cloud Run `--timeout`) as the only bound. |
|
|
269
277
|
| `per_tool_timeout_seconds` | `None` | Caps each individual tool call made from within the sandbox. |
|
|
270
278
|
| `session_idle_timeout_seconds` | `600` | Idle reaper: closes a turn's container once it goes untouched this long. Backstop for turns that never call `release_invocation`. |
|
|
271
279
|
|
|
@@ -350,38 +358,6 @@ Tags are indented; their content is not. The `<tools-package>` body is Python so
|
|
|
350
358
|
|
|
351
359
|
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.
|
|
352
360
|
|
|
353
|
-
#### Schema fidelity
|
|
354
|
-
|
|
355
|
-
Stubs are rendered from each tool's declaration, so whatever the declaration drops can never reach the model. Nested object properties, their descriptions and JSON Schema's validation keywords all land in the docstring, because no Python type expresses them:
|
|
356
|
-
|
|
357
|
-
~~~python
|
|
358
|
-
def create_minimum_spend_rule(*, space_id: int, conditions: dict[str, Any] | None = ..., label: str | None = ...) -> Any:
|
|
359
|
-
"""Creates a new minimum spend rule under the MinimumSpend policy.
|
|
360
|
-
|
|
361
|
-
Args:
|
|
362
|
-
space_id: The unique identifier of the venue space. (format=int32)
|
|
363
|
-
conditions: Conditions that must all be satisfied for the rule to apply (AND logic).
|
|
364
|
-
daysOfWeek: list[int] | None - Days of the week (1=Monday, 7=Sunday). (items: minimum=1, maximum=7)
|
|
365
|
-
timeRange: dict[str, Any] | None - Time window of the booking start time.
|
|
366
|
-
from: str (pattern=^([01][0-9]|2[0-3]):[0-5][0-9]$, example=18:00)
|
|
367
|
-
to: str (example=23:59)
|
|
368
|
-
label: (maxLength=255)
|
|
369
|
-
"""
|
|
370
|
-
~~~
|
|
371
|
-
|
|
372
|
-
Every JSON Schema 2020-12 Validation and Meta-Data keyword is covered; `tests/test_stubs_json_schema.py` reads the keyword set from the meta-schemas and fails if one goes unclassified.
|
|
373
|
-
|
|
374
|
-
> [!IMPORTANT]
|
|
375
|
-
> **Enable ADK's `JSON_SCHEMA_FOR_FUNC_DECL` feature to get the full picture.** It is off by default, and without it ADK builds declarations through a Gemini `types.Schema`, which reduces an `allOf`-wrapped `$ref` — how OpenAPI 3.0 spells a nullable reference — to a bare `{"type": "OBJECT"}`. The referenced properties and description are gone before this package sees them, and the field renders as an empty `dict[str, Any]`.
|
|
376
|
-
>
|
|
377
|
-
> ```python
|
|
378
|
-
> from google.adk.features import FeatureName, override_feature_enabled
|
|
379
|
-
>
|
|
380
|
-
> override_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True)
|
|
381
|
-
> ```
|
|
382
|
-
>
|
|
383
|
-
> Or set `ADK_ENABLE_JSON_SCHEMA_FOR_FUNC_DECL=1` in the environment. Do this before building any tool. It changes how declarations are built for every tool in the process, so if the same process also exposes tools to the model directly, check the model layer accepts `parameters_json_schema` — ADK's LiteLLM integration does, and `ExecuteCodeTool` declares itself that way regardless.
|
|
384
|
-
|
|
385
361
|
#### Fitting the budget
|
|
386
362
|
|
|
387
363
|
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.
|
|
@@ -97,6 +97,11 @@ class _BlockConnectionLost(Exception):
|
|
|
97
97
|
|
|
98
98
|
logger = logging.getLogger("adk_code_mode.tool")
|
|
99
99
|
|
|
100
|
+
# A block is a model-authored program, so it is bounded by default: an unbounded one
|
|
101
|
+
# strands its container (the idle reaper skips turns still in use) and holds up
|
|
102
|
+
# whatever is waiting on the turn. Hosts that genuinely need longer pass their own.
|
|
103
|
+
DEFAULT_TIMEOUT_SECONDS = 60
|
|
104
|
+
|
|
100
105
|
_REAPER_MAX_POLL_SECONDS = 30.0
|
|
101
106
|
|
|
102
107
|
# One block runs at most twice: the original attempt plus a single reconnect, and
|
|
@@ -202,7 +207,7 @@ class ExecuteCodeTool(BaseTool):
|
|
|
202
207
|
max_code_mode_metadata_chars: int = 50_000,
|
|
203
208
|
max_output_chars: int = 50_000,
|
|
204
209
|
max_code_chars: int = 1_000_000,
|
|
205
|
-
timeout_seconds: int | None =
|
|
210
|
+
timeout_seconds: int | None = DEFAULT_TIMEOUT_SECONDS,
|
|
206
211
|
per_tool_timeout_seconds: float | None = None,
|
|
207
212
|
session_idle_timeout_seconds: float = 600,
|
|
208
213
|
on_artifacts_saved: ArtifactsSavedCallback | None = None,
|
|
@@ -241,7 +246,9 @@ class ExecuteCodeTool(BaseTool):
|
|
|
241
246
|
max_code_chars: Rejects oversized code payloads before starting a
|
|
242
247
|
container.
|
|
243
248
|
timeout_seconds: Caps overall execution time of one ``execute_code``
|
|
244
|
-
call.
|
|
249
|
+
call, and with it every tool the block calls. ``None`` lifts the cap,
|
|
250
|
+
which leaves a runaway block stranding its container until the idle
|
|
251
|
+
reaper takes it.
|
|
245
252
|
per_tool_timeout_seconds: Caps each individual tool call made from
|
|
246
253
|
within the sandbox.
|
|
247
254
|
session_idle_timeout_seconds: Idle reaper: closes a turn's container
|
|
@@ -26,7 +26,7 @@ class ToolSurfaceError(ValueError):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class DuplicateToolNameError(ToolSurfaceError):
|
|
29
|
-
"""Raised when two
|
|
29
|
+
"""Raised when two tools in the same namespace share a raw ADK tool name."""
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class NamespaceCollisionError(ToolSurfaceError):
|
|
@@ -91,23 +91,22 @@ def build(tools: list[ResolvedTool]) -> list[NamespacedTool]:
|
|
|
91
91
|
"""
|
|
92
92
|
out: list[NamespacedTool] = []
|
|
93
93
|
seen_attributes: set[tuple[str | None, str]] = set()
|
|
94
|
-
seen_raw_names: dict[str, NamespacedTool] = {}
|
|
94
|
+
seen_raw_names: dict[tuple[str | None, str], NamespacedTool] = {}
|
|
95
95
|
namespace_sources: dict[str, BaseToolset] = {}
|
|
96
96
|
bare_attributes: dict[str, NamespacedTool] = {}
|
|
97
97
|
for resolved in tools:
|
|
98
98
|
namespace = _toolset_namespace(resolved.toolset) if resolved.toolset else None
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
else _sanitise_identifier(resolved.tool.name, fallback="tool")
|
|
105
|
-
)
|
|
99
|
+
# Raw names only have to be unique *within* a namespace: dispatch goes
|
|
100
|
+
# through the dotted path, so two toolsets may each expose ``list_items``.
|
|
101
|
+
raw_key = (namespace, resolved.tool.name)
|
|
102
|
+
if raw_key in seen_raw_names:
|
|
103
|
+
other = seen_raw_names[raw_key]
|
|
106
104
|
raise DuplicateToolNameError(
|
|
107
|
-
f"Duplicate ADK tool name {resolved.tool.name!r}
|
|
108
|
-
f"
|
|
109
|
-
f"{
|
|
110
|
-
"Rename one of the tools or apply distinct
|
|
105
|
+
f"Duplicate ADK tool name {resolved.tool.name!r} within namespace "
|
|
106
|
+
f"{namespace or 'tools'!r} is not supported; this would make host dispatch "
|
|
107
|
+
f"ambiguous for {other.dotted_path!r} (from {_origin(other.resolved)} and "
|
|
108
|
+
f"{_origin(resolved)}). Rename one of the tools or apply distinct "
|
|
109
|
+
"``tool_name_prefix`` values."
|
|
111
110
|
)
|
|
112
111
|
if namespace is not None and resolved.toolset is not None:
|
|
113
112
|
existing_source = namespace_sources.get(namespace)
|
|
@@ -138,7 +137,7 @@ def build(tools: list[ResolvedTool]) -> list[NamespacedTool]:
|
|
|
138
137
|
)
|
|
139
138
|
seen_attributes.add(key)
|
|
140
139
|
nt = NamespacedTool(resolved=resolved, namespace=namespace, attribute=attribute)
|
|
141
|
-
seen_raw_names[
|
|
140
|
+
seen_raw_names[raw_key] = nt
|
|
142
141
|
if namespace is None:
|
|
143
142
|
bare_attributes[attribute] = nt
|
|
144
143
|
out.append(nt)
|
|
@@ -167,15 +166,12 @@ class Registry:
|
|
|
167
166
|
|
|
168
167
|
def __init__(self, tools: list[NamespacedTool]) -> None:
|
|
169
168
|
self._by_path: dict[str, NamespacedTool] = {t.dotted_path: t for t in tools}
|
|
170
|
-
|
|
169
|
+
# Generated stubs call ``_call(<dotted path>, ...)``, so this map is only a
|
|
170
|
+
# fallback for callers that know the raw ADK name. A name shared by two
|
|
171
|
+
# namespaces is ambiguous and simply doesn't resolve through it.
|
|
172
|
+
by_tool_name: dict[str, NamespacedTool | None] = {}
|
|
171
173
|
for tool in tools:
|
|
172
|
-
|
|
173
|
-
if existing is not None:
|
|
174
|
-
raise DuplicateToolNameError(
|
|
175
|
-
"Duplicate ADK tool name "
|
|
176
|
-
f"{tool.tool_name!r} is not supported; use unique tool names."
|
|
177
|
-
)
|
|
178
|
-
by_tool_name[tool.tool_name] = tool
|
|
174
|
+
by_tool_name[tool.tool_name] = None if tool.tool_name in by_tool_name else tool
|
|
179
175
|
self._by_tool_name = by_tool_name
|
|
180
176
|
self._tools = list(tools)
|
|
181
177
|
|
|
@@ -187,10 +183,11 @@ class Registry:
|
|
|
187
183
|
return self._by_path.get(path)
|
|
188
184
|
|
|
189
185
|
def by_tool_name(self, tool_name: str) -> NamespacedTool | None:
|
|
186
|
+
"""Resolve a raw ADK name, or ``None`` if unknown or shared by two namespaces."""
|
|
190
187
|
return self._by_tool_name.get(tool_name)
|
|
191
188
|
|
|
192
189
|
def resolve_call(self, name: str) -> NamespacedTool:
|
|
193
|
-
"""Accept either the dotted path or
|
|
190
|
+
"""Accept either the dotted path or an unambiguous original ``BaseTool.name``."""
|
|
194
191
|
hit = self._by_path.get(name) or self._by_tool_name.get(name)
|
|
195
192
|
if hit is None:
|
|
196
193
|
raise KeyError(f"no tool registered for {name!r}")
|
|
@@ -151,10 +151,9 @@ def _effective_schema(schema: Any) -> dict[str, Any]:
|
|
|
151
151
|
if isinstance(variants, list):
|
|
152
152
|
merged: dict[str, Any] = {}
|
|
153
153
|
for part in variants:
|
|
154
|
-
merged
|
|
155
|
-
#
|
|
156
|
-
merged
|
|
157
|
-
normalised = merged
|
|
154
|
+
merged = _merge_schema(merged, _effective_schema(part))
|
|
155
|
+
# Wrapper keys (e.g. `nullable`) win; `properties` / `required` still merge.
|
|
156
|
+
normalised = _merge_schema(merged, normalised)
|
|
158
157
|
|
|
159
158
|
# A one-variant union expresses no choice, so fold it in rather than lose
|
|
160
159
|
# the description and properties hanging off the single branch.
|
|
@@ -163,12 +162,42 @@ def _effective_schema(schema: Any) -> dict[str, Any]:
|
|
|
163
162
|
if isinstance(branches, list) and len(branches) == 1:
|
|
164
163
|
folded = _effective_schema(branches[0])
|
|
165
164
|
normalised.pop(key)
|
|
166
|
-
folded
|
|
167
|
-
normalised = folded
|
|
165
|
+
normalised = _merge_schema(folded, normalised)
|
|
168
166
|
|
|
169
167
|
return normalised
|
|
170
168
|
|
|
171
169
|
|
|
170
|
+
def _merge_schema(base: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any]:
|
|
171
|
+
"""Combine two schemas the way ``allOf`` means: properties accumulate.
|
|
172
|
+
|
|
173
|
+
A shallow ``update`` replaces the whole ``properties`` dict, so OpenAPI 3.0
|
|
174
|
+
``allOf: [$ref, {properties: extra}]`` would keep only ``extra``.
|
|
175
|
+
"""
|
|
176
|
+
out = dict(base)
|
|
177
|
+
extra_props = extra.get("properties")
|
|
178
|
+
if isinstance(extra_props, dict):
|
|
179
|
+
props = dict(out.get("properties") or {})
|
|
180
|
+
for name, schema in extra_props.items():
|
|
181
|
+
existing = props.get(name)
|
|
182
|
+
if isinstance(existing, dict) and isinstance(schema, dict):
|
|
183
|
+
props[name] = _merge_schema(existing, schema)
|
|
184
|
+
else:
|
|
185
|
+
props[name] = schema
|
|
186
|
+
out["properties"] = props
|
|
187
|
+
extra_required = extra.get("required")
|
|
188
|
+
if isinstance(extra_required, list):
|
|
189
|
+
required = [name for name in (out.get("required") or []) if isinstance(name, str)]
|
|
190
|
+
for name in extra_required:
|
|
191
|
+
if isinstance(name, str) and name not in required:
|
|
192
|
+
required.append(name)
|
|
193
|
+
out["required"] = required
|
|
194
|
+
for key, value in extra.items():
|
|
195
|
+
if key in ("properties", "required"):
|
|
196
|
+
continue
|
|
197
|
+
out[key] = value
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
172
201
|
def _schema_to_type(schema: Any) -> str:
|
|
173
202
|
"""Convert a JSON-Schema fragment to a Python type expression (PEP 604).
|
|
174
203
|
|
|
@@ -322,23 +351,93 @@ def _prose(schema: dict[str, Any]) -> str:
|
|
|
322
351
|
return ""
|
|
323
352
|
|
|
324
353
|
|
|
325
|
-
def
|
|
326
|
-
"""
|
|
354
|
+
def _const_or_single_enum(schema: dict[str, Any]) -> str | None:
|
|
355
|
+
"""A branch tag value: ``const`` or a one-element ``enum``."""
|
|
356
|
+
if "const" in schema:
|
|
357
|
+
return _format_scalar(schema["const"])
|
|
358
|
+
enum = schema.get("enum")
|
|
359
|
+
if isinstance(enum, list) and len(enum) == 1:
|
|
360
|
+
return _format_scalar(enum[0])
|
|
361
|
+
return None
|
|
327
362
|
|
|
328
|
-
|
|
329
|
-
|
|
363
|
+
|
|
364
|
+
def _union_branch_tag(branch: dict[str, Any], *, discriminator: str | None) -> str | None:
|
|
365
|
+
"""Label a union branch without implying a nested object key.
|
|
366
|
+
|
|
367
|
+
OpenAPI discriminator unions (``type: openapi | mcp``) often lose
|
|
368
|
+
``discriminator`` once ADK inlines ``$ref`` into ``items``, leaving only a
|
|
369
|
+
single-value ``type`` enum on each object. Tag as ``(type=openapi)`` rather
|
|
370
|
+
than a fake ``openapi:`` key the model might nest under.
|
|
330
371
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
372
|
+
props = branch.get("properties")
|
|
373
|
+
if not isinstance(props, dict):
|
|
374
|
+
return None
|
|
375
|
+
names: list[str] = []
|
|
376
|
+
if discriminator:
|
|
377
|
+
names.append(discriminator)
|
|
378
|
+
if "type" not in names:
|
|
379
|
+
names.append("type")
|
|
380
|
+
for name in names:
|
|
381
|
+
raw = props.get(name)
|
|
382
|
+
if not isinstance(raw, dict):
|
|
383
|
+
continue
|
|
384
|
+
value = _const_or_single_enum(_effective_schema(raw))
|
|
385
|
+
if value is not None:
|
|
386
|
+
return f"{name}={value}"
|
|
387
|
+
return None
|
|
333
388
|
|
|
389
|
+
|
|
390
|
+
def _describe_union(
|
|
391
|
+
schema: dict[str, Any], variants: list[Any], *, indent: str, depth: int
|
|
392
|
+
) -> list[str]:
|
|
393
|
+
"""Expand object ``oneOf``/``anyOf`` branches into the docstring.
|
|
394
|
+
|
|
395
|
+
The signature collapses every object variant to ``dict[str, Any]``, so
|
|
396
|
+
without this the keys on a discriminator union never reach the model.
|
|
397
|
+
Primitive-only unions stay in the type expression and are skipped here.
|
|
398
|
+
"""
|
|
399
|
+
disc = schema.get("discriminator")
|
|
400
|
+
discriminator = None
|
|
401
|
+
if isinstance(disc, dict):
|
|
402
|
+
name = disc.get("propertyName")
|
|
403
|
+
if isinstance(name, str) and name:
|
|
404
|
+
discriminator = name
|
|
405
|
+
|
|
406
|
+
parent_required = schema.get("required")
|
|
407
|
+
lines: list[str] = []
|
|
408
|
+
for raw in variants:
|
|
409
|
+
branch = _effective_schema(raw)
|
|
410
|
+
if isinstance(parent_required, list):
|
|
411
|
+
branch = _merge_schema(branch, {"required": parent_required})
|
|
412
|
+
nested = _describe_fields(branch, indent=indent, depth=depth + 1)
|
|
413
|
+
if not nested:
|
|
414
|
+
continue
|
|
415
|
+
if lines:
|
|
416
|
+
lines.append(f"{indent}-- or --")
|
|
417
|
+
tag = _union_branch_tag(branch, discriminator=discriminator)
|
|
418
|
+
description = _prose(branch).splitlines()
|
|
419
|
+
suffix = _constraint_suffix(branch)
|
|
420
|
+
if tag:
|
|
421
|
+
head = f"{indent}({tag})"
|
|
422
|
+
if description:
|
|
423
|
+
head += f" - {description[0].strip()}"
|
|
424
|
+
lines.append(head + suffix)
|
|
425
|
+
elif description:
|
|
426
|
+
lines.append(f"{indent}{description[0].strip()}{suffix}")
|
|
427
|
+
elif suffix:
|
|
428
|
+
lines.append(f"{indent}{suffix.strip()}")
|
|
429
|
+
for extra in description[1:]:
|
|
430
|
+
lines.append(f"{indent} {extra.strip()}")
|
|
431
|
+
lines.extend(nested)
|
|
432
|
+
return lines
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _describe_properties(schema: dict[str, Any], *, indent: str, depth: int) -> list[str]:
|
|
436
|
+
"""One line per object property, plus nested expansion."""
|
|
334
437
|
properties = schema.get("properties")
|
|
335
438
|
if not isinstance(properties, dict) or not properties:
|
|
336
|
-
items = schema.get("items")
|
|
337
|
-
if isinstance(items, dict):
|
|
338
|
-
return _describe_fields(_effective_schema(items), indent=indent, depth=depth + 1)
|
|
339
439
|
return []
|
|
340
|
-
|
|
341
|
-
required = set(schema.get("required") or [])
|
|
440
|
+
required = {name for name in (schema.get("required") or []) if isinstance(name, str)}
|
|
342
441
|
lines: list[str] = []
|
|
343
442
|
for name, raw in properties.items():
|
|
344
443
|
field = _effective_schema(raw)
|
|
@@ -355,6 +454,63 @@ def _describe_fields(schema: dict[str, Any], *, indent: str, depth: int = 0) ->
|
|
|
355
454
|
return lines
|
|
356
455
|
|
|
357
456
|
|
|
457
|
+
def _describe_prefix_items(schema: dict[str, Any], *, indent: str, depth: int) -> list[str]:
|
|
458
|
+
"""Expand object (or union) slots of a ``prefixItems`` tuple.
|
|
459
|
+
|
|
460
|
+
Primitive slots are already in the ``tuple[...]`` type expression.
|
|
461
|
+
"""
|
|
462
|
+
prefix_items = schema.get("prefixItems")
|
|
463
|
+
if not isinstance(prefix_items, list) or not prefix_items:
|
|
464
|
+
return []
|
|
465
|
+
lines: list[str] = []
|
|
466
|
+
for index, raw in enumerate(prefix_items):
|
|
467
|
+
field = _effective_schema(raw)
|
|
468
|
+
nested = _describe_fields(field, indent=indent + " ", depth=depth + 1)
|
|
469
|
+
if not nested:
|
|
470
|
+
continue
|
|
471
|
+
head = f"{indent}[{index}]: {_schema_to_type(field)}"
|
|
472
|
+
description = _prose(field).splitlines()
|
|
473
|
+
if description:
|
|
474
|
+
head += f" - {description[0].strip()}"
|
|
475
|
+
lines.append(head + _constraint_suffix(field, include_default=True))
|
|
476
|
+
for extra in description[1:]:
|
|
477
|
+
lines.append(f"{indent} {extra.strip()}")
|
|
478
|
+
lines.extend(nested)
|
|
479
|
+
return lines
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _describe_fields(schema: dict[str, Any], *, indent: str, depth: int = 0) -> list[str]:
|
|
483
|
+
"""Lines describing an object's properties, recursing into nested schemas.
|
|
484
|
+
|
|
485
|
+
The signature can only ever say ``dict[str, Any]`` for an object, so without
|
|
486
|
+
this the key names, their types and their meanings never reach the model.
|
|
487
|
+
Multi-branch ``oneOf``/``anyOf`` is walked the same way: a single-branch
|
|
488
|
+
union is folded in ``_effective_schema``, but two object variants have no
|
|
489
|
+
``properties`` at the union itself. Shared parent ``properties`` are listed
|
|
490
|
+
first, then the branches.
|
|
491
|
+
"""
|
|
492
|
+
if depth >= _MAX_SCHEMA_DEPTH:
|
|
493
|
+
return []
|
|
494
|
+
|
|
495
|
+
schema = _effective_schema(schema)
|
|
496
|
+
|
|
497
|
+
variants = schema.get("anyOf") or schema.get("oneOf")
|
|
498
|
+
lines = _describe_properties(schema, indent=indent, depth=depth)
|
|
499
|
+
if isinstance(variants, list) and len(variants) > 1:
|
|
500
|
+
lines.extend(_describe_union(schema, variants, indent=indent, depth=depth))
|
|
501
|
+
if lines:
|
|
502
|
+
return lines
|
|
503
|
+
|
|
504
|
+
prefix_lines = _describe_prefix_items(schema, indent=indent, depth=depth)
|
|
505
|
+
if prefix_lines:
|
|
506
|
+
return prefix_lines
|
|
507
|
+
|
|
508
|
+
items = schema.get("items")
|
|
509
|
+
if isinstance(items, dict):
|
|
510
|
+
return _describe_fields(items, indent=indent, depth=depth + 1)
|
|
511
|
+
return lines
|
|
512
|
+
|
|
513
|
+
|
|
358
514
|
def _format_docstring(
|
|
359
515
|
*, description: str, param_docs: list[tuple[str, dict[str, Any], str | None]]
|
|
360
516
|
) -> str:
|
|
@@ -83,6 +83,18 @@ def test_render_catalog_includes_top_level_tools_section() -> None:
|
|
|
83
83
|
assert "def echo(*, message: str) -> Any:" in catalog
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def test_render_catalog_keeps_a_shared_tool_name_in_both_namespaces() -> None:
|
|
87
|
+
bookings = _FakeToolset("bookings")
|
|
88
|
+
inventory = _FakeToolset("inventory")
|
|
89
|
+
a = _SchemaTool("list_items", description="List bookings.", schema={"type": "object"})
|
|
90
|
+
b = _SchemaTool("list_items", description="List inventory.", schema={"type": "object"})
|
|
91
|
+
catalog = render_catalog(_build([(a, bookings), (b, inventory)]))
|
|
92
|
+
assert "from tools.bookings import list_items" in catalog
|
|
93
|
+
assert "from tools.inventory import list_items" in catalog
|
|
94
|
+
assert '"""List bookings.' in catalog
|
|
95
|
+
assert '"""List inventory.' in catalog
|
|
96
|
+
|
|
97
|
+
|
|
86
98
|
def test_render_catalog_groups_tools_by_namespace_and_sorts() -> None:
|
|
87
99
|
slack = _FakeToolset("slack")
|
|
88
100
|
gmail = _FakeToolset("gmail")
|
|
@@ -24,7 +24,11 @@ from google.genai import types as genai_types
|
|
|
24
24
|
|
|
25
25
|
from adk_code_mode.runtime.base import SandboxConnectionError, SandboxResult, SandboxSession
|
|
26
26
|
from adk_code_mode.runtime.protocol import PROTOCOL_VERSION, DoneFrame, Frame, ReadyFrame
|
|
27
|
-
from adk_code_mode.tool import
|
|
27
|
+
from adk_code_mode.tool import (
|
|
28
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
29
|
+
ExecuteCodeTool,
|
|
30
|
+
ProtocolVersionMismatchError,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
from ._fake_runtime import FakeRuntime
|
|
30
34
|
|
|
@@ -362,6 +366,11 @@ async def test_timeout_terminates_hung_sandbox() -> None:
|
|
|
362
366
|
assert "Execution exceeded timeout of 1s" in result["stderr"]
|
|
363
367
|
|
|
364
368
|
|
|
369
|
+
def test_a_block_is_bounded_without_the_host_saying_so() -> None:
|
|
370
|
+
tool = ExecuteCodeTool(tools=[], backend=FakeRuntime())
|
|
371
|
+
assert tool.timeout_seconds == DEFAULT_TIMEOUT_SECONDS
|
|
372
|
+
|
|
373
|
+
|
|
365
374
|
@pytest.mark.asyncio
|
|
366
375
|
async def test_timeout_does_not_wait_for_in_flight_tool_call() -> None:
|
|
367
376
|
artifact_service = InMemoryArtifactService()
|
|
@@ -54,15 +54,46 @@ def test_toolset_tools_share_namespace() -> None:
|
|
|
54
54
|
assert ns[1].dotted_path == "slack.list_channels"
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def test_duplicate_raw_tool_names_in_one_namespace_raise() -> None:
|
|
58
|
+
slack = _NamedToolset("Slack")
|
|
59
|
+
tools = [
|
|
60
|
+
ResolvedTool(tool=_Tool("ping"), toolset=slack),
|
|
61
|
+
ResolvedTool(tool=_Tool("ping"), toolset=slack),
|
|
62
|
+
]
|
|
63
|
+
with pytest.raises(namespacing.DuplicateToolNameError):
|
|
64
|
+
namespacing.build(tools)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_duplicate_bare_tool_names_raise() -> None:
|
|
58
68
|
tools = [
|
|
59
69
|
ResolvedTool(tool=_Tool("ping"), toolset=None),
|
|
60
|
-
ResolvedTool(tool=_Tool("ping"), toolset=
|
|
70
|
+
ResolvedTool(tool=_Tool("ping"), toolset=None),
|
|
61
71
|
]
|
|
62
72
|
with pytest.raises(namespacing.DuplicateToolNameError):
|
|
63
73
|
namespacing.build(tools)
|
|
64
74
|
|
|
65
75
|
|
|
76
|
+
def test_same_tool_name_in_two_namespaces_is_allowed() -> None:
|
|
77
|
+
tools = [
|
|
78
|
+
ResolvedTool(tool=_Tool("list_items"), toolset=_NamedToolset("Bookings")),
|
|
79
|
+
ResolvedTool(tool=_Tool("list_items"), toolset=_NamedToolset("Inventory")),
|
|
80
|
+
]
|
|
81
|
+
ns = namespacing.build(tools)
|
|
82
|
+
assert [t.dotted_path for t in ns] == ["bookings.list_items", "inventory.list_items"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_bare_tool_may_share_a_name_with_a_namespaced_tool() -> None:
|
|
86
|
+
tools = [
|
|
87
|
+
ResolvedTool(tool=_Tool("ping"), toolset=None),
|
|
88
|
+
ResolvedTool(tool=_Tool("ping"), toolset=_NamedToolset("Slack")),
|
|
89
|
+
]
|
|
90
|
+
ns = namespacing.build(tools)
|
|
91
|
+
assert [t.dotted_path for t in ns] == ["ping", "slack.ping"]
|
|
92
|
+
reg = namespacing.Registry(ns)
|
|
93
|
+
assert reg.resolve_call("ping") is ns[0]
|
|
94
|
+
assert reg.resolve_call("slack.ping") is ns[1]
|
|
95
|
+
|
|
96
|
+
|
|
66
97
|
def test_python_identifier_collisions_raise() -> None:
|
|
67
98
|
tools = [
|
|
68
99
|
ResolvedTool(tool=_Tool("ping"), toolset=None),
|
|
@@ -98,3 +129,18 @@ def test_registry_bidirectional_lookup() -> None:
|
|
|
98
129
|
assert reg.resolve_call("send_message") is ns[0]
|
|
99
130
|
with pytest.raises(KeyError):
|
|
100
131
|
reg.resolve_call("nope")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_registry_dotted_paths_still_resolve_when_a_name_is_shared() -> None:
|
|
135
|
+
ns = namespacing.build(
|
|
136
|
+
[
|
|
137
|
+
ResolvedTool(tool=_Tool("list_items"), toolset=_NamedToolset("Bookings")),
|
|
138
|
+
ResolvedTool(tool=_Tool("list_items"), toolset=_NamedToolset("Inventory")),
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
reg = namespacing.Registry(ns)
|
|
142
|
+
assert reg.resolve_call("bookings.list_items") is ns[0]
|
|
143
|
+
assert reg.resolve_call("inventory.list_items") is ns[1]
|
|
144
|
+
assert reg.by_tool_name("list_items") is None
|
|
145
|
+
with pytest.raises(KeyError):
|
|
146
|
+
reg.resolve_call("list_items")
|
|
@@ -224,6 +224,35 @@ class TestApplicatorVocabulary:
|
|
|
224
224
|
required=["pair"],
|
|
225
225
|
)
|
|
226
226
|
assert "pair: tuple[str, int]" in source
|
|
227
|
+
# Primitive slots are already in the type; don't repeat them.
|
|
228
|
+
assert "[0]:" not in source
|
|
229
|
+
assert "[1]:" not in source
|
|
230
|
+
|
|
231
|
+
def test_prefix_items_object_slots_expand_their_properties(self) -> None:
|
|
232
|
+
source = _render(
|
|
233
|
+
{
|
|
234
|
+
"pair": {
|
|
235
|
+
"type": "array",
|
|
236
|
+
"prefixItems": [
|
|
237
|
+
{"type": "string"},
|
|
238
|
+
{
|
|
239
|
+
"type": "object",
|
|
240
|
+
"required": ["lat", "lng"],
|
|
241
|
+
"properties": {
|
|
242
|
+
"lat": {"type": "number"},
|
|
243
|
+
"lng": {"type": "number"},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
required=["pair"],
|
|
250
|
+
)
|
|
251
|
+
assert "pair: tuple[str, dict[str, Any]]" in source
|
|
252
|
+
assert "[1]: dict[str, Any]" in source
|
|
253
|
+
assert "lat: float (required)" in source
|
|
254
|
+
assert "lng: float (required)" in source
|
|
255
|
+
assert "[0]:" not in source
|
|
227
256
|
|
|
228
257
|
def test_additional_properties_schema_types_an_open_map(self) -> None:
|
|
229
258
|
source = _render(
|
|
@@ -268,6 +297,29 @@ class TestApplicatorVocabulary:
|
|
|
268
297
|
assert "Min/max capacity range." in source
|
|
269
298
|
assert "min: int" in source
|
|
270
299
|
|
|
300
|
+
def test_allof_merges_properties_from_each_branch(self) -> None:
|
|
301
|
+
"""OpenAPI 3.0 extends a `$ref` with extra fields via a second `allOf`
|
|
302
|
+
object; those keys must join the first object's, not replace them."""
|
|
303
|
+
source = _render(
|
|
304
|
+
{
|
|
305
|
+
"page": {
|
|
306
|
+
"allOf": [
|
|
307
|
+
{
|
|
308
|
+
"type": "object",
|
|
309
|
+
"required": ["limit"],
|
|
310
|
+
"properties": {"limit": {"type": "integer"}},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"required": ["cursor"],
|
|
314
|
+
"properties": {"cursor": {"type": "string"}},
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
assert "limit: int (required)" in source
|
|
321
|
+
assert "cursor: str (required)" in source
|
|
322
|
+
|
|
271
323
|
def test_anyof_becomes_a_union_and_a_lone_branch_is_folded(self) -> None:
|
|
272
324
|
source = _render(
|
|
273
325
|
{
|
|
@@ -279,6 +331,185 @@ class TestApplicatorVocabulary:
|
|
|
279
331
|
assert "many: str | int" in source
|
|
280
332
|
assert "one: str" in source
|
|
281
333
|
assert "Only branch." in source
|
|
334
|
+
# Primitive unions belong in the type expression, not a second listing.
|
|
335
|
+
assert "-- or --" not in source
|
|
336
|
+
|
|
337
|
+
def test_oneof_object_branches_expand_their_properties(self) -> None:
|
|
338
|
+
"""A discriminator union has no `properties` at the `oneOf` itself.
|
|
339
|
+
|
|
340
|
+
Folding only the single-branch case leaves `list[dict[str, Any]]` with
|
|
341
|
+
an empty docstring — the shape of `create_agent(tools=...)`.
|
|
342
|
+
"""
|
|
343
|
+
source = _render(
|
|
344
|
+
{
|
|
345
|
+
"tools": {
|
|
346
|
+
"type": "array",
|
|
347
|
+
"items": {
|
|
348
|
+
"oneOf": [
|
|
349
|
+
{
|
|
350
|
+
"type": "object",
|
|
351
|
+
"additionalProperties": False,
|
|
352
|
+
"required": ["type", "specUrl"],
|
|
353
|
+
"properties": {
|
|
354
|
+
"type": {"type": "string", "enum": ["openapi"]},
|
|
355
|
+
"specUrl": {"type": "string"},
|
|
356
|
+
"operationOverrides": {
|
|
357
|
+
"type": "array",
|
|
358
|
+
"items": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"required": ["sourceId", "enabled"],
|
|
361
|
+
"properties": {
|
|
362
|
+
"sourceId": {"type": "string"},
|
|
363
|
+
"enabled": {"type": "boolean"},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"type": "object",
|
|
371
|
+
"additionalProperties": False,
|
|
372
|
+
"required": ["type", "url"],
|
|
373
|
+
"properties": {
|
|
374
|
+
"type": {"type": "string", "enum": ["mcp"]},
|
|
375
|
+
"url": {"type": "string"},
|
|
376
|
+
"toolOverrides": {
|
|
377
|
+
"type": "array",
|
|
378
|
+
"items": {
|
|
379
|
+
"type": "object",
|
|
380
|
+
"required": ["sourceId", "enabled"],
|
|
381
|
+
"properties": {
|
|
382
|
+
"sourceId": {"type": "string"},
|
|
383
|
+
"enabled": {"type": "boolean"},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
required=["tools"],
|
|
394
|
+
)
|
|
395
|
+
assert "tools: list[dict[str, Any]]" in source
|
|
396
|
+
assert "(type=openapi) (no other keys)" in source
|
|
397
|
+
assert "specUrl: str (required)" in source
|
|
398
|
+
assert "operationOverrides: list[dict[str, Any]]" in source
|
|
399
|
+
assert "-- or --" in source
|
|
400
|
+
assert "(type=mcp) (no other keys)" in source
|
|
401
|
+
assert "url: str (required)" in source
|
|
402
|
+
assert "toolOverrides: list[dict[str, Any]]" in source
|
|
403
|
+
assert "sourceId: str (required)" in source
|
|
404
|
+
assert "enabled: bool (required)" in source
|
|
405
|
+
|
|
406
|
+
def test_oneof_uses_discriminator_property_for_the_branch_tag(self) -> None:
|
|
407
|
+
source = _render(
|
|
408
|
+
{
|
|
409
|
+
"tool": {
|
|
410
|
+
"discriminator": {"propertyName": "kind"},
|
|
411
|
+
"oneOf": [
|
|
412
|
+
{
|
|
413
|
+
"type": "object",
|
|
414
|
+
"properties": {
|
|
415
|
+
"kind": {"const": "http"},
|
|
416
|
+
"path": {"type": "string"},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"type": "object",
|
|
421
|
+
"properties": {
|
|
422
|
+
"kind": {"const": "stdio"},
|
|
423
|
+
"command": {"type": "string"},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
assert "(kind=http)" in source
|
|
431
|
+
assert "(kind=stdio)" in source
|
|
432
|
+
assert "path: str" in source
|
|
433
|
+
assert "command: str" in source
|
|
434
|
+
|
|
435
|
+
def test_parent_required_applies_to_oneof_branch_properties(self) -> None:
|
|
436
|
+
source = _render(
|
|
437
|
+
{
|
|
438
|
+
"target": {
|
|
439
|
+
"required": ["value"],
|
|
440
|
+
"oneOf": [
|
|
441
|
+
{
|
|
442
|
+
"type": "object",
|
|
443
|
+
"properties": {"value": {"type": "string"}},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"type": "object",
|
|
447
|
+
"properties": {"value": {"type": "integer"}},
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
assert "value: str (required)" in source
|
|
454
|
+
assert "value: int (required)" in source
|
|
455
|
+
|
|
456
|
+
def test_oneof_branch_prose_is_preserved(self) -> None:
|
|
457
|
+
source = _render(
|
|
458
|
+
{
|
|
459
|
+
"target": {
|
|
460
|
+
"oneOf": [
|
|
461
|
+
{
|
|
462
|
+
"type": "object",
|
|
463
|
+
"title": "Email target",
|
|
464
|
+
"properties": {"value": {"type": "string"}},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"type": "object",
|
|
468
|
+
"description": "Phone target.",
|
|
469
|
+
"properties": {"value": {"type": "string"}},
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
assert "Email target" in source
|
|
476
|
+
assert "Phone target." in source
|
|
477
|
+
|
|
478
|
+
def test_shared_properties_are_listed_before_oneof_branches(self) -> None:
|
|
479
|
+
source = _render(
|
|
480
|
+
{
|
|
481
|
+
"payment": {
|
|
482
|
+
"properties": {"id": {"type": "string"}},
|
|
483
|
+
"required": ["id"],
|
|
484
|
+
"oneOf": [
|
|
485
|
+
{
|
|
486
|
+
"type": "object",
|
|
487
|
+
"properties": {
|
|
488
|
+
"type": {"type": "string", "enum": ["card"]},
|
|
489
|
+
"cardNumber": {"type": "string"},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"type": "object",
|
|
494
|
+
"properties": {
|
|
495
|
+
"type": {"type": "string", "enum": ["bank"]},
|
|
496
|
+
"iban": {"type": "string"},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
assert "id: str (required)" in source
|
|
504
|
+
assert "(type=card)" in source
|
|
505
|
+
assert "cardNumber: str" in source
|
|
506
|
+
assert "(type=bank)" in source
|
|
507
|
+
assert "iban: str" in source
|
|
508
|
+
id_at = source.index("id: str (required)")
|
|
509
|
+
card_at = source.index("(type=card)")
|
|
510
|
+
or_at = source.index("-- or --")
|
|
511
|
+
bank_at = source.index("(type=bank)")
|
|
512
|
+
assert id_at < card_at < or_at < bank_at
|
|
282
513
|
|
|
283
514
|
|
|
284
515
|
class TestMetaDataVocabulary:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py
RENAMED
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py
RENAMED
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py
RENAMED
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py
RENAMED
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py
RENAMED
|
File without changes
|
{adk_code_mode-1.4.0 → adk_code_mode-1.6.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|