adk-code-mode 1.4.0__tar.gz → 1.5.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 (78) hide show
  1. adk_code_mode-1.5.0/.release-please-manifest.json +3 -0
  2. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/CHANGELOG.md +7 -0
  3. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/PKG-INFO +11 -35
  4. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/README.md +10 -34
  5. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
  6. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/__about__.py +1 -1
  7. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/namespacing.py +20 -23
  8. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_catalog.py +12 -0
  9. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_namespacing.py +48 -2
  10. adk_code_mode-1.4.0/.release-please-manifest.json +0 -3
  11. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.githooks/pre-commit +0 -0
  12. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.githooks/pre-push +0 -0
  13. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.github/workflows/ci.yml +0 -0
  14. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.github/workflows/release-please.yml +0 -0
  15. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.gitignore +0 -0
  16. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.prettierrc +0 -0
  17. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.vscode/settings.json +0 -0
  18. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/.zed/settings.json +0 -0
  19. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/LICENSE +0 -0
  20. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/Makefile +0 -0
  21. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/docker/Dockerfile +0 -0
  22. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/pyproject.toml +0 -0
  23. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/release-please-config.json +0 -0
  24. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/README.md +0 -0
  25. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/pyproject.toml +0 -0
  26. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
  27. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
  28. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +0 -0
  29. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +0 -0
  30. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
  31. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/sandbox-wheel/src/adk_code_mode_sandbox/protocol.py +0 -0
  32. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/scripts/install-git-hooks.sh +0 -0
  33. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/__init__.py +0 -0
  34. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/_artifact_tools.py +0 -0
  35. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/metadata.py +0 -0
  36. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/output.py +0 -0
  37. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/py.typed +0 -0
  38. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/runtime/__init__.py +0 -0
  39. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/runtime/base.py +0 -0
  40. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/runtime/docker.py +0 -0
  41. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/runtime/protocol.py +0 -0
  42. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/runtime/remote.py +0 -0
  43. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tool.py +0 -0
  44. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tool_result_artifacts.py +0 -0
  45. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/__init__.py +0 -0
  46. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/catalog.py +0 -0
  47. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/dispatcher.py +0 -0
  48. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/normaliser.py +0 -0
  49. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/tools/stubs.py +0 -0
  50. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/workspace/__init__.py +0 -0
  51. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/src/adk_code_mode/workspace/files.py +0 -0
  52. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/__init__.py +0 -0
  53. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/_docker_helpers.py +0 -0
  54. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/_fake_runtime.py +0 -0
  55. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/conftest.py +0 -0
  56. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_agent_with_fake_llm.py +0 -0
  57. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_artifact_tools.py +0 -0
  58. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_dispatcher.py +0 -0
  59. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_docker_image.py +0 -0
  60. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_docker_integration.py +0 -0
  61. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_host_loop.py +0 -0
  62. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_integration.py +0 -0
  63. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_metadata.py +0 -0
  64. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_normaliser.py +0 -0
  65. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_output.py +0 -0
  66. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_protocol.py +0 -0
  67. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_remote_backend.py +0 -0
  68. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_remote_integration.py +0 -0
  69. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_runtime_docker.py +0 -0
  70. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_sandbox_entry.py +0 -0
  71. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_sandbox_rpc_client.py +0 -0
  72. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_stubs.py +0 -0
  73. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_stubs_json_schema.py +0 -0
  74. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_tool.py +0 -0
  75. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_tool_result_artifacts.py +0 -0
  76. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_version.py +0 -0
  77. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/tests/test_workspace_files.py +0 -0
  78. {adk_code_mode-1.4.0 → adk_code_mode-1.5.0}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.5.0"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [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)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+
3
10
  ## [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
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adk-code-mode
3
- Version: 1.4.0
3
+ Version: 1.5.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", # your deployed sandbox URL
94
- token="your-secret-token", # bearer token for auth
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"),
@@ -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", # your deployed sandbox URL
67
- token="your-secret-token", # bearer token for auth
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"),
@@ -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.
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "1.4.0" # x-release-please-version
4
+ __version__ = "1.5.0" # x-release-please-version
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "1.4.0" # x-release-please-version
4
+ __version__ = "1.5.0" # x-release-please-version
@@ -26,7 +26,7 @@ class ToolSurfaceError(ValueError):
26
26
 
27
27
 
28
28
  class DuplicateToolNameError(ToolSurfaceError):
29
- """Raised when two resolved tools share the same raw ADK tool name."""
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
- if resolved.tool.name in seen_raw_names:
100
- other = seen_raw_names[resolved.tool.name]
101
- candidate = (
102
- f"{namespace}.{_sanitise_identifier(resolved.tool.name, fallback='tool')}"
103
- if namespace
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} is not supported; this would "
108
- f"make host dispatch ambiguous between {other.dotted_path!r} (from "
109
- f"{_origin(other.resolved)}) and {candidate!r} (from {_origin(resolved)}). "
110
- "Rename one of the tools or apply distinct ``tool_name_prefix`` values."
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[resolved.tool.name] = nt
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
- by_tool_name: dict[str, NamespacedTool] = {}
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
- existing = by_tool_name.get(tool.tool_name)
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 the original ``BaseTool.name``."""
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}")
@@ -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")
@@ -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 test_duplicate_raw_tool_names_raise() -> None:
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=_NamedToolset("Slack")),
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")
@@ -1,3 +0,0 @@
1
- {
2
- ".": "1.4.0"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes