Conversation
FunctionTool only awaits a callable when inspect.iscoroutinefunction() is
true, and that check does not see through a plain sync decorator such as
`@functools.wraps(fn) def wrapper(*a, **k): return fn(*a, **k)`. The sync
branch then returned the coroutine unawaited, so the tool body never ran
and the model received `{'result': <coroutine object ...>}`.
Await the result of the sync path when it is awaitable, matching how
before/after tool callbacks already handle user callables.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to Issue or Description of Change
2. Or, if no issue exists, describe the change:
Problem:
FunctionTool._invoke_callabledecides whether to await a tool withinspect.iscoroutinefunction(). That check does not see through a plain sync decorator, which is a common pattern for logging, retries or rate limiting:The declaration is still built correctly (
inspect.signaturefollows__wrapped__), so the model calls the tool normally. But the sync branch returns the coroutine without awaiting it, so the tool body never runs and the model receives:Solution:
On the sync path (both the direct call and the bound sync-callable runner), await the result if it is awaitable. This matches how ADK already treats user callables elsewhere, e.g. before/after tool callbacks in
utils/_callback_pipeline.py. Regular sync and async tools are unaffected.Related: #7012 handles an awaitable returned by a
require_confirmationpredicate by failing closed. With this change_invoke_callableawaits such a result, so a sync wrapper around an async predicate returns its real bool.Testing Plan
Unit Tests:
Added to
tests/unittests/tools/test_function_tool.py:test_run_async_awaits_async_function_behind_sync_wrappertest_run_async_awaits_async_function_behind_sync_wrapper_with_runner(same case with a thread-pool sync callable runner bound)Both fail on
mainwithassert <coroutine object ...> == {'item': 'apple', 'price': 3}and pass with this change.The 3 failures are
tests/unittests/tools/test_skill_toolset.py::test_integration_shell_*, which also fail onmainon my machine (Windows, no/bin/bash), so they are unrelated.Manual End-to-End (E2E) Tests:
The agent above, run through
InMemoryRunnerwith a mock model that callsget_price(item="apple"):Before (
main):After:
Checklist