Under arun(), Agno's FunctionCall.aexecute() builds the tool hook chain from async wrappers, so the function_call a hook receives returns a coroutine. A sync hook written as return function_call(**arguments) can't await it. The hook returned the unawaited coroutine, and Agno treated that as the tool result.
The tool body never ran. The model got <coroutine object ...> as the tool output, and any exception the tool would have raised disappeared. The same thing happened to a sync tool as soon as any hook in the stack was async, since that also sends the call through aexecute().
This script runs one hook through the async path, with no model:
import asyncio
from agno.tools.function import Function, FunctionCall
calls = []
async def get_weather(city: str) -> str:
calls.append(city)
return f"Sunny in {city}"
def logging_hook(function_name, func, args):
print(f"[logging_hook] Calling {function_name} with {args}")
return func(**args)
fc = FunctionCall(function=Function.from_callable(get_weather), arguments={"city": "Tokyo"})
fc.function.tool_hooks = [logging_hook]
async def main():
result = await fc.aexecute()
print("result:", result.result)
print("tool body ran:", bool(calls))
asyncio.run(main())Before (real output)
[logging_hook] Calling get_weather with {'city': 'Tokyo'}
result: <coroutine object FunctionCall._build_nested_execution_chain_async.<locals>.create_hook_wrapper.<locals>.wrapper.<locals>.next_func at 0x107e6ac50>
tool body ran: False
Now (real output)
[logging_hook] Calling get_weather with {'city': 'Tokyo'}
result: Sunny in Tokyo
tool body ran: TrueAgno now awaits any awaitable a hook returns, at every level of the async chain. Async generators and plain short-circuit values pass through untouched, and the sync chain is unchanged.
See the cookbook, and learn more about tool hooks in the documentation.
Frequently asked questions
In the async path, Agno hands each tool hook an async continuation. A sync hook that returned function_call(**arguments) returned an unawaited coroutine, and Agno used it as the tool result. Agno now awaits that value, so the tool body runs and the model gets its real output.
No. A sync hook that returns function_call(**arguments) now works under arun(), because Agno awaits whatever awaitable a hook hands back, at each level of the async hook chain.
No. The sync hook chain is unchanged. The fix applies to the async chain in FunctionCall.aexecute().



