From 61c90b51e532bb03e82e0ba16aa90d46ee23afde Mon Sep 17 00:00:00 2001 From: haruotsu <65439874+haruotsu@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:49:56 +0900 Subject: [PATCH 1/2] Test that ADK carries the bound tool call into each tool body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tools that never get a tool_context reach the user's tokens through the call that before_tool_callback binds in a contextvar. That only holds while ADK runs the callback and the tool body in the same context, one per call. Nothing drove that through ADK's own dispatch, so a release that moved the callback elsewhere would have left every such tool without a token, with the unit tests still passing. 🤖 Generated with Claude Code --- tests/test_runtime_build.py | 53 +++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/test_runtime_build.py b/tests/test_runtime_build.py index 58ed998..12aa9d3 100644 --- a/tests/test_runtime_build.py +++ b/tests/test_runtime_build.py @@ -514,6 +514,15 @@ async def dispatched(agent: Any, called: str) -> Any: tools it resolved for the turn, the callback it awaits - so one test drives the real thing and fails if any of that moves. """ + return (await dispatched_together(agent, [called]))[0] + + +async def dispatched_together(agent: Any, called: list[str]) -> list[Any]: + """What ADK answers function calls the model made in one turn. + + ADK runs calls from one turn concurrently, so this is where calls can + interleave. They get ids call-1, call-2, ... in the order given. + """ from google.adk.agents.invocation_context import InvocationContext from google.adk.agents.run_config import RunConfig from google.adk.flows.llm_flows.functions import handle_function_call_list_async @@ -539,11 +548,14 @@ async def dispatched(agent: Any, called: str) -> Any: assert invocation.canonical_tools_cache is not None event = await handle_function_call_list_async( invocation, - [types.FunctionCall(id="call-1", name=called, args={})], + [ + types.FunctionCall(id=f"call-{number}", name=name, args={}) + for number, name in enumerate(called, start=1) + ], request.tools_dict, ) assert event is not None - return event.content.parts[0].function_response.response + return [part.function_response.response for part in event.content.parts] async def test_adk_answers_a_made_up_name_with_the_tools_it_resolved( @@ -562,6 +574,43 @@ async def test_adk_answers_a_made_up_name_with_the_tools_it_resolved( } +def write_call_reading_module(directory: Path) -> None: + package = directory / "src" / "call_reader" + package.mkdir(parents=True) + (package / "__init__.py").write_text("") + (package / "tools.py").write_text( + "import asyncio\n" + "\n" + "from gete.request_context import current_tool_call\n" + "\n" + "async def whose_call() -> dict:\n" + ' """Name the call this tool runs in."""\n' + " await asyncio.sleep(0.01)\n" + " call = current_tool_call()\n" + ' return {"call": call.tool_context.function_call_id if call else None}\n' + "\n" + "TOOLS = [whose_call]\n" + ) + + +async def test_each_call_in_a_turn_sees_only_the_call_adk_bound_it_to( + project: ProjectBuilder, +) -> None: + """Through ADK: what the callback binds reaches the tool body, and two + calls running together do not see each other's - the user's tokens + travel this way to tools that never get a context.""" + path = resolved_path( + project, + "mail-triage", + {"source": "./src", "tools": [{"python": "call_reader.tools:TOOLS"}]}, + ) + write_call_reading_module(path.parent) + assert await dispatched_together(build(path), ["whose_call", "whose_call"]) == [ + {"call": "call-1"}, + {"call": "call-2"}, + ] + + async def test_an_arbitrary_gete_error_is_generic_like_any_other( project: ProjectBuilder, ) -> None: From c8d43f2538295b851730cfac9a8c75fc9866df45 Mon Sep 17 00:00:00 2001 From: haruotsu <65439874+haruotsu@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:49:56 +0900 Subject: [PATCH 2/2] Allow google-adk up to 2.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The range stopped at 2.9, and 2.9 holds opentelemetry-api at 1.42.1, which has a published advisory fixed in 1.45.1. ADK 2.14 allows 1.45.2. The 2.10 to 2.14 changes were checked against what gete relies on. The OpenAPI query style and SSRF changes are in RestApiTool's request path, which gete does not use: it takes only the declaration and sends through its own client. Instruction templating now substitutes less. No gete tool takes Enum arguments. The bound tool call still reaches each tool body, which the previous commit now tests through ADK's own dispatch. google-genai moves to 2.34.0 because ADK 2.14 requires it. 🤖 Generated with Claude Code --- pyproject.toml | 2 +- uv.lock | 33 +++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ca33e11..6153613 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ # deployed runtime stays inside it as well. # The mcp extra is what makes mcp: tools work; without it the toolset # module fails to import where the agent is deployed. - "google-adk[mcp]>=2.6,<2.10", + "google-adk[mcp]>=2.6,<2.15", # Signs a GitHub App's JWT for the app connections. ADK brings it in # already; it is named because gete imports it itself. "cryptography>=43", diff --git a/uv.lock b/uv.lock index d605517..d6b141a 100644 --- a/uv.lock +++ b/uv.lock @@ -735,7 +735,7 @@ dev = [ requires-dist = [ { name = "click", marker = "extra == 'cli'", specifier = ">=8.1" }, { name = "cryptography", specifier = ">=43" }, - { name = "google-adk", extras = ["mcp"], specifier = ">=2.6,<2.10" }, + { name = "google-adk", extras = ["mcp"], specifier = ">=2.6,<2.15" }, { name = "google-auth", marker = "extra == 'cli'", specifier = ">=2.38" }, { name = "httpx", specifier = ">=0.28" }, { name = "jsonschema", specifier = ">=4.23" }, @@ -755,12 +755,13 @@ dev = [ [[package]] name = "google-adk" -version = "2.9.2" +version = "2.14.0" source = { registry = "https://pypi.flatt.tech/simple/" } dependencies = [ { name = "aiosqlite" }, { name = "authlib" }, { name = "click" }, + { name = "cryptography" }, { name = "fastapi" }, { name = "google-auth", extra = ["pyopenssl"] }, { name = "google-genai" }, @@ -782,9 +783,9 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://pypi.flatt.tech/files/packages/db/08/7ac5e31305b9c0e1714af8ea8c1a5df16e980090eed5e5e456dc8fe5b583/google_adk-2.9.2.tar.gz", hash = "sha256:9d4b91a3cc3a52a471ca5831127e4c46ac174da0db519b4d157a09011737da10", size = 4002237, upload-time = "2026-09-18T18:10:46.112Z" } +sdist = { url = "https://pypi.flatt.tech/files/packages/ca/b4/79b0b2140d45274a7d0e502cf0aa8d01d671e9c2f35d2eb96df2165d8c96/google_adk-2.14.0.tar.gz", hash = "sha256:c6762050a8d1edac10f03730f99500733e862b729422dbd63187154dc470b7f0", size = 4574051, upload-time = "2026-11-07T01:03:34.755Z" } wheels = [ - { url = "https://pypi.flatt.tech/files/packages/23/a6/e375f8a124e28f31c0cd0f9ff8607d7bdc652f8ce4bc67c39863353b4c6d/google_adk-2.9.2-py3-none-any.whl", hash = "sha256:ad887aadff34ebfe06586ded2b04848413c8bed6d6c7dd14dc7485ec7e54b2d2", size = 4631024, upload-time = "2026-09-18T18:10:43.492Z" }, + { url = "https://pypi.flatt.tech/files/packages/9f/06/4329fd6fda8c85ad3f70256d502d45d636a125e85fb4a85848a98d061938/google_adk-2.14.0-py3-none-any.whl", hash = "sha256:f7d8c79bafc4f9215402219c9542db0bde702952448a09ffd51c5bd22b135b0e", size = 5302590, upload-time = "2026-11-07T01:03:32.405Z" }, ] [package.optional-dependencies] @@ -817,7 +818,7 @@ requests = [ [[package]] name = "google-genai" -version = "2.22.0" +version = "2.34.0" source = { registry = "https://pypi.flatt.tech/simple/" } dependencies = [ { name = "anyio" }, @@ -831,9 +832,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://pypi.flatt.tech/files/packages/37/f1/f2f31b2a6bd826bc2cb73068df880954a026474c03a4315637d20cc13965/google_genai-2.22.0.tar.gz", hash = "sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd", size = 684719, upload-time = "2026-09-02T18:06:02.906Z" } +sdist = { url = "https://pypi.flatt.tech/files/packages/22/89/4621aee9cb183efa5b8a187f0e315c49fddcfb8cd7f238b4a3bf4946dcd0/google_genai-2.34.0.tar.gz", hash = "sha256:8e182cf3f73384167b96861cf0aef5102b5070f2db1ea11002879e913806ef37", size = 791853, upload-time = "2026-10-29T06:43:23.706Z" } wheels = [ - { url = "https://pypi.flatt.tech/files/packages/de/96/0120d214958cb54f2b9c48da9f564a0e6eae269e9dd702415fb1d0551cc7/google_genai-2.22.0-py3-none-any.whl", hash = "sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6", size = 1088792, upload-time = "2026-09-02T18:06:00.629Z" }, + { url = "https://pypi.flatt.tech/files/packages/2e/42/c24db865d9231b1c672ea26242a4dcf340e200bb1a9823dce4aa9c4c07a4/google_genai-2.34.0-py3-none-any.whl", hash = "sha256:a4cb02aebc4c357f863d2938132f0e3f7967fc5e00b8a4078673f68aad340665", size = 1258155, upload-time = "2026-10-29T06:43:21.121Z" }, ] [[package]] @@ -1277,41 +1278,41 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.42.1" +version = "1.45.2" source = { registry = "https://pypi.flatt.tech/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.flatt.tech/files/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +sdist = { url = "https://pypi.flatt.tech/files/packages/19/14/cf3a6b6448849656e1e5f4e2101f3988dc6789724050096aba1bc5ccf592/opentelemetry_api-1.45.2.tar.gz", hash = "sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4", size = 73277, upload-time = "2026-10-29T17:29:37.888Z" } wheels = [ - { url = "https://pypi.flatt.tech/files/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, + { url = "https://pypi.flatt.tech/files/packages/a9/2d/610db864d7513158cebb8f36cc88e67e73bd6c7aa1905ef18d89e1281c81/opentelemetry_api-1.45.2-py3-none-any.whl", hash = "sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34", size = 60335, upload-time = "2026-10-29T17:29:06.955Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.42.1" +version = "1.45.2" source = { registry = "https://pypi.flatt.tech/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.flatt.tech/files/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +sdist = { url = "https://pypi.flatt.tech/files/packages/7b/1a/74a21d4d6969fb8b432ac397e66112a8167a6deb267ea4559dbaf2fe91ef/opentelemetry_sdk-1.45.2.tar.gz", hash = "sha256:7eec4de13dd21ff3a1670dcb0e02079e692736f94d733dfdce5af461d884248d", size = 221705, upload-time = "2026-10-29T17:29:54.685Z" } wheels = [ - { url = "https://pypi.flatt.tech/files/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, + { url = "https://pypi.flatt.tech/files/packages/9f/a5/9a3b793ef48245f2336a16beb2324c2397dbb3f987cd68942f77cca06156/opentelemetry_sdk-1.45.2-py3-none-any.whl", hash = "sha256:ff974ea70ee4f1d51e2ec152db37c2b626c7bf82bb3f7d86114fd8704e7199c6", size = 141756, upload-time = "2026-10-29T17:29:33.722Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.63b1" +version = "0.66b2" source = { registry = "https://pypi.flatt.tech/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.flatt.tech/files/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +sdist = { url = "https://pypi.flatt.tech/files/packages/37/3f/50f95972b50e8e353f5b9342034c9cd17730f1956f009ce6a3fbbd89f9ce/opentelemetry_semantic_conventions-0.66b2.tar.gz", hash = "sha256:7da5f1a5dc4383eb66711d4d3d3e71824d9403ac1559fea021cc103c1a4c4976", size = 150246, upload-time = "2026-10-29T17:29:55.682Z" } wheels = [ - { url = "https://pypi.flatt.tech/files/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, + { url = "https://pypi.flatt.tech/files/packages/67/bd/6bf33ed31521c565931c037e9e81bac47c1acb000e850c6e24220e2a0c00/opentelemetry_semantic_conventions-0.66b2-py3-none-any.whl", hash = "sha256:4cbe61e5f9275d57ab31708446e7938ef4291ebbe6a7fca6f7a629c5acae87d8", size = 206280, upload-time = "2026-10-29T17:29:35.062Z" }, ] [[package]]