Skip to content

fix(tools): resolve awaitables returned by synchronous wrappers - #7506

Open
BichengWang wants to merge 4 commits into
google:mainfrom
BichengWang:BichengWang/contribute-pr-review-poc
Open

BichengWang wants to merge 4 commits into
google:mainfrom
BichengWang:BichengWang/contribute-pr-review-poc

Conversation

@BichengWang

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

Problem: FunctionTool.run_async() only awaits a callable when inspect.iscoroutinefunction() recognizes the callable (or its __call__). A synchronous wrapper or factory returning a coroutine/Future therefore returns the unresolved awaitable as the tool result. The async body never executes. The shared invocation helper also treats a synchronous wrapper around an async confirmation predicate as a truthy coroutine, so a resolved False incorrectly requests confirmation.

Expected: Return the resolved tool result, propagate errors from the awaitable, and use the resolved confirmation decision.

Solution: After invoking a synchronous callable, await its result when inspect.isawaitable() is true. Do this after both inline and thread-pool invocation so the synchronous wrapper still runs in the worker while its asynchronous continuation runs on the caller's event loop. Existing async-function and generator dispatch is unchanged; no public signatures or configuration options change.

Update the existing FunctionTool guide and add regressions for coroutine/Future results, asynchronous exceptions, confirmation decisions, and thread-pool event-loop ownership.

Minimal reproduction

On unmodified main (bca4a425b), Python 3.14.7:

import asyncio
from unittest.mock import MagicMock
from google.adk.tools import FunctionTool

async def fetch(value: str) -> str:
    return f"resolved:{value}"

def wrapped_tool(value: str):
    return fetch(value)

async def main():
    result = await FunctionTool(wrapped_tool).run_async(
        args={"value": "hello"}, tool_context=MagicMock()
    )
    print(type(result).__name__)
    assert result == "resolved:hello"

asyncio.run(main())

Before: prints coroutine; assertion fails and the coroutine is never awaited.
After: prints str; returns resolved:hello.

Testing Plan

Unit Tests:

  • Added behavior regressions in the existing test modules.
  • All three affected test modules pass locally on Python 3.14.7:
.venv/bin/pytest \
  tests/unittests/tools/test_function_tool.py \
  tests/unittests/flows/llm_flows/tools/test_functions_thread_pool.py \
  tests/unittests/flows/llm_flows/tools/test_functions_simple.py -q
# 225 passed

The coroutine/Future result and exception-propagation tests failed on the unmodified implementation before applying the production fix. Existing synchronous, asynchronous, generator, live-mode and thread-pool coverage remains passing. The full repository suite was not run.

Manual End-to-End (E2E) Tests:

Ran a real Runner with an Agent, InMemorySessionService, and a local scripted BaseLlm (no network/model credentials). The model emits a wrapped_tool(value="hello") function call, then a final done response. The synchronous tool wrapper returns a coroutine that verifies its event loop, writes tool_context.state["resolved"], and returns a dictionary. A synchronous confirmation wrapper returns a coroutine resolving to False.

Exercised both RunConfig() and RunConfig(tool_thread_pool_config=ToolThreadPoolConfig()). Checked the serializable FunctionResponse, feedback in the second model request, persisted session state, one tool invocation, and final agent response:

PASS pool=False: response={'resolved': 'hello'}, persisted_state={'resolved': 'hello'}, wrapper_thread=MainThread, async_result_on_caller_loop=True
PASS pool=True: response={'resolved': 'hello'}, persisted_state={'resolved': 'hello'}, wrapper_thread=adk_tool_executor_0, async_result_on_caller_loop=True

Checklist

  • Read CONTRIBUTING.md.
  • Performed a self-review.
  • Commented the non-obvious awaitable-result handling.
  • Added tests that reproduce the failure and verify the fix.
  • New and existing tests in the affected modules pass locally.
  • Manually exercised the changed path through Runner.
  • Updated the existing FunctionTool guide; no new public API or downstream dependency changes.

Await results from synchronous FunctionTool callables after inline or thread-pool invocation. This executes wrapped async tools and evaluates wrapped confirmation predicates using their resolved decisions instead of coroutine objects.
@BichengWang

Copy link
Copy Markdown
Contributor Author

@GWeale @xuanyang15 could you review this focused FunctionTool invocation fix?

I selected you based on #6799: George reviewed that tool-dispatch change and authored the landed FunctionTool thread-pool preflight fix (0156bc5a9); Xuan approved #6799 and has authored FunctionTool confirmation changes. This PR touches the same invocation helper and covers both thread-pool event-loop ownership and confirmation decisions.

The failure is a synchronous wrapper returning an unresolved coroutine/Future. The fix awaits that result after inline or worker invocation, without changing async-function/generator dispatch or adding a configuration flag.

Verification: 225 affected-module tests passed; real Runner smoke runs passed with the tool pool off/on, including serialized tool feedback, persisted state, and a wrapped async confirmation predicate resolving to False. Pre-commit checks passed. Full-suite execution was not performed.

BichengWang and others added 3 commits October 14, 2026 00:00
Keep the upstream invocation helper, which already awaits synchronous wrapper results, and retain the regression tests and documentation.
Keep the upstream decorated-function tests alongside coverage for returned Futures, awaitable failures, confirmation predicates and caller-loop ownership.
@BichengWang

Copy link
Copy Markdown
Contributor Author

Merged current main and resolved the test conflict, keeping both sets of regression tests. The invocation fix is already on main; this PR now adds tests and documentation. The two affected test modules pass (155 tests), and pre-commit passes.

@GWeale could you review the remaining coverage and documentation, and approve the pending CI workflow?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants