Skip to content

fix(core): await coroutines returned by callable token getters and bound params#736

Open
hsusul wants to merge 1 commit into
googleapis:mainfrom
hsusul:fix/resolve-value-awaitable-callables
Open

fix(core): await coroutines returned by callable token getters and bound params#736
hsusul wants to merge 1 commit into
googleapis:mainfrom
hsusul:fix/resolve-value-awaitable-callables

Conversation

@hsusul

@hsusul hsusul commented Jul 25, 2026

Copy link
Copy Markdown

Problem & Minimal Reproduction

In toolbox-core, resolve_value is responsible for resolving dynamic sources (such as auth_service_token_getters, bound_params, and client_headers) into values before invoking remote tools or building client headers.

The public type annotations and docstrings explicitly support Callable[[], Awaitable[Any]]:
source: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]

However, when a callable is not a raw async def function (for example: a callable class instance with async def __call__(self), a lambda returning an awaitable like lambda: get_token_async(), or a function returning a coroutine), asyncio.iscoroutinefunction(source) evaluates to False.

When resolve_value executes elif callable(source): return source(), source() returns an Awaitable (coroutine object) which is never awaited. resolve_value returns <coroutine object ...> instead of the resolved value string/object, emitting a RuntimeWarning: coroutine '...' was never awaited and causing transport header or payload errors (TypeError / invalid header format).

Minimal Reproduction:

import asyncio
from toolbox_core.utils import resolve_value

class CallableAsyncGetter:
    async def __call__(self) -> str:
        return "my-token"

async def main():
    getter = CallableAsyncGetter()
    token = await resolve_value(getter)
    # Current behavior: token is <coroutine object CallableAsyncGetter.__call__ ...>
    # Correct behavior: token is "my-token"

asyncio.run(main())

Current vs. Corrected Behavior

  • Current Behavior: Passing a callable object or a function/lambda returning an Awaitable causes resolve_value to return an unawaited <coroutine object ...>, triggering a runtime warning and failing when constructing headers or payloads.
  • Corrected Behavior: resolve_value inspects the return value of any callable and awaits it if inspect.isawaitable(res) is True, ensuring all coroutines/awaitables are resolved to their underlying values.

Root Cause

resolve_value relied solely on asyncio.iscoroutinefunction(source) to determine if an await was required. In Python, callable objects implementing async def __call__, lambdas returning awaitables, functools.partial wrappers, and functions returning coroutines do not satisfy asyncio.iscoroutinefunction(source).

Minimal Implementation

Updated resolve_value in packages/toolbox-core/src/toolbox_core/utils.py:

async def resolve_value(
    source: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any],
) -> Any:
    if asyncio.iscoroutinefunction(source):
        return await source()
    elif callable(source):
        res = source()
        if inspect.isawaitable(res):
            return await res
        return res
    return source

Tests

Added test_resolve_value_callable_returning_awaitable to packages/toolbox-core/tests/test_utils.py verifying that callable class instances with async def __call__ and lambdas returning awaitables resolve to their expected string values without runtime warnings.

Validation Results

  • Unit tests:
    • toolbox-core: 479 passed in 0.81s (pytest -k "not e2e")
    • toolbox-adk: 54 passed in 0.73s (pytest tests/unit)
    • toolbox-langchain: 60 passed in 0.20s (pytest -k "not e2e")
    • toolbox-llamaindex: 61 passed in 0.77s (pytest -k "not e2e")
  • Code quality:
    • black --check .: Passed
    • isort --check .: Passed
    • mypy: Passed across all 4 package modules (toolbox_core, toolbox_adk, toolbox_langchain, toolbox_llamaindex)
    • git diff --check: Clean

Adapter Compatibility

All framework adapters (toolbox-adk, toolbox-langchain, toolbox-llamaindex) delegate token resolution and bound parameters to toolbox-core's resolve_value. This fix restores proper async token resolution for custom callable objects and lambdas across all adapters.

Issue Creation Limitation

No standalone GitHub issue was created prior to opening this PR because issue creation permissions on googleapis/mcp-toolbox-sdk-python are restricted to repository maintainers.

Explicit Non-Goals

  • No changes to underlying HTTP transports or protocol negotiation.
  • No changes to sync/async API signatures.

@hsusul
hsusul requested a review from a team as a code owner July 25, 2026 02:01
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