Upstream: microsoft#5741 — microsoft#5741
Imported by: github-upstreamer
Problem (verbatim from upstream)
Tools are currently executed directly inside the agent async event loop.
If a tool contains blocking synchronous code (for example time.sleep(120) or any long-running sync I/O), the entire event loop gets blocked.
In our case, this prevents Azure Bot from polling the Responses API (GET endpoint) for ResponsesAgentServerHost (azure.ai.agentserver.responses), because the polling mechanism is running inside the same event loop.
Current behavior
At the moment, tool execution appears to behave like this (around lines 682 and 733 of https://github.com/microsoft/agent-framework/blob/main/python/packages/core/agent_framework/_tools.py):
res = self.__call__(**call_kwargs)
result = await res if inspect.isawaitable(res) else res
The issue is that the synchronous function is already executed before checking whether the result is awaitable.
This means that blocking synchronous code blocks the event loop immediately.
Reproduction example
import asyncio
import datetime
import time
from agent_framework import tool
def _get_current_date() -> str:
time.sleep(120)
return datetime.datetime.now().strftime("%A, %d %B %Y")
@tool(approval_mode="never_require")
async def get_current_date() -> str:
"""Returns today's date and day of the week."""
return await asyncio.to_thread(_get_current_date)
# Sync version - actually not working because blocks eventloop
# @tool(approval_mode="never_require")
# def get_current_date() -> str:
# time.sleep(120)
# return datetime.datetime.now().strftime("%A, %d %B %Y")
When this tool is executed by the agent, the entire async loop becomes blocked for 120 seconds.
As a consequence:
- Responses API polling stops
- Azure Bot cannot retrieve updates
- The agent appears frozen until the sync function returns
Verified workarounds
We verified that everything works correctly when:
- The tool is implemented asynchronously
async def tool():
await asyncio.sleep(120)
- The synchronous tool is explicitly executed in a thread
await asyncio.to_thread(blocking_tool)
Expected behavior (verbatim from upstream)
- Async tools should continue running normally in the event loop
- Sync tools should automatically be executed in a worker thread
- Responses API polling should remain responsive even during long-running synchronous tool execution
Upstream triage status
The upstream automated triage has confirmed reproduction (reproduced label). The triage bot verified that FunctionTool.invoke() blocks the async event loop when the wrapped function is synchronous.
Investigation Notes
The blocking call occurs in FunctionTool.invoke() where synchronous tool functions are called directly on the event loop thread without offloading to a worker.
Affected Layers & Files
Core Tool Execution
python/packages/core/agent_framework/_tools.py — Contains FunctionTool class. The invoke method (line 562+) calls self.__call__(**call_kwargs) at lines 682 and 733 (with and without observability enabled). The __call__ method (line 511+) calls self.func() synchronously. The call chain from invoke through to the user's function runs entirely on the event loop thread.
python/packages/core/agent_framework/_tools.py — Also contains _auto_invoke_function (line 1411+), _try_execute_function_calls (line 1632+), and _execute_function_calls (line 1781+) which form the upstream call chain reaching invoke.
Agent Layer
python/packages/core/agent_framework/_agents.py — MCP tool invocation at line 1543 calls agent_tool.invoke() in the same async context.
Hosting / Server Layer
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py — ResponsesHostServer._handle_inner_agent (line 322) runs agent.run() on the async event loop. This is the same event loop that handles Responses API polling, so blocking tool execution stops polling.
Existing Pattern (Workflows)
python/packages/core/agent_framework/_workflows/_function_executor.py — The FunctionExecutor class (lines 137-151) already handles this correctly for workflow functions by wrapping sync functions with asyncio.to_thread(). This demonstrates the codebase already has an established pattern for offloading sync functions.
Tests
python/packages/core/tests/core/test_tools.py — General tool tests; no tests covering the sync-blocking-event-loop scenario.
python/packages/core/tests/core/test_tools_future_annotations.py — Annotation-related tool tests.
python/packages/core/tests/workflow/test_function_executor.py — Tests for the workflow executor (which handles sync offloading correctly).
Additional Context
- The
FunctionTool.invoke() method has two code paths that both exhibit the blocking call: one when observability is disabled (line 682) and one when it's enabled (line 733). Both need to be addressed.
- The
__call__ method itself is synchronous and calls self.func() directly — the blocking happens before the inspect.isawaitable() check on the next line can make any difference.
- The
_function_executor.py workflow module already demonstrates a working pattern for detecting sync vs async functions and offloading sync ones to threads.
- Package version reported:
agent-framework: 1.3.0.
Acceptance Criteria
- [derived from problem statement] Synchronous tool functions registered via
@tool do not block the async event loop during execution — concurrent async tasks (including polling) continue to make progress while a sync tool runs
- [derived from problem statement] Asynchronous tool functions continue to execute normally in the event loop without behavioral change
- [derived from problem statement] Responses API polling remains responsive during long-running synchronous tool execution when using
ResponsesAgentServerHost
- [derived from problem statement] Existing async tools that return awaitables are not affected by changes to sync tool handling
- [derived from problem statement] Both invoke code paths (with and without observability) handle sync functions without blocking the event loop
Problem (verbatim from upstream)
Tools are currently executed directly inside the agent async event loop.
If a tool contains blocking synchronous code (for example
time.sleep(120)or any long-running sync I/O), the entire event loop gets blocked.In our case, this prevents Azure Bot from polling the Responses API (
GETendpoint) for ResponsesAgentServerHost (azure.ai.agentserver.responses), because the polling mechanism is running inside the same event loop.Current behavior
At the moment, tool execution appears to behave like this (around lines 682 and 733 of https://github.com/microsoft/agent-framework/blob/main/python/packages/core/agent_framework/_tools.py):
The issue is that the synchronous function is already executed before checking whether the result is awaitable.
This means that blocking synchronous code blocks the event loop immediately.
Reproduction example
When this tool is executed by the agent, the entire async loop becomes blocked for 120 seconds.
As a consequence:
Verified workarounds
We verified that everything works correctly when:
Expected behavior (verbatim from upstream)
Upstream triage status
The upstream automated triage has confirmed reproduction (
reproducedlabel). The triage bot verified thatFunctionTool.invoke()blocks the async event loop when the wrapped function is synchronous.Investigation Notes
The blocking call occurs in
FunctionTool.invoke()where synchronous tool functions are called directly on the event loop thread without offloading to a worker.Affected Layers & Files
Core Tool Execution
python/packages/core/agent_framework/_tools.py— ContainsFunctionToolclass. Theinvokemethod (line 562+) callsself.__call__(**call_kwargs)at lines 682 and 733 (with and without observability enabled). The__call__method (line 511+) callsself.func()synchronously. The call chain from invoke through to the user's function runs entirely on the event loop thread.python/packages/core/agent_framework/_tools.py— Also contains_auto_invoke_function(line 1411+),_try_execute_function_calls(line 1632+), and_execute_function_calls(line 1781+) which form the upstream call chain reachinginvoke.Agent Layer
python/packages/core/agent_framework/_agents.py— MCP tool invocation at line 1543 callsagent_tool.invoke()in the same async context.Hosting / Server Layer
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py—ResponsesHostServer._handle_inner_agent(line 322) runsagent.run()on the async event loop. This is the same event loop that handles Responses API polling, so blocking tool execution stops polling.Existing Pattern (Workflows)
python/packages/core/agent_framework/_workflows/_function_executor.py— TheFunctionExecutorclass (lines 137-151) already handles this correctly for workflow functions by wrapping sync functions withasyncio.to_thread(). This demonstrates the codebase already has an established pattern for offloading sync functions.Tests
python/packages/core/tests/core/test_tools.py— General tool tests; no tests covering the sync-blocking-event-loop scenario.python/packages/core/tests/core/test_tools_future_annotations.py— Annotation-related tool tests.python/packages/core/tests/workflow/test_function_executor.py— Tests for the workflow executor (which handles sync offloading correctly).Additional Context
FunctionTool.invoke()method has two code paths that both exhibit the blocking call: one when observability is disabled (line 682) and one when it's enabled (line 733). Both need to be addressed.__call__method itself is synchronous and callsself.func()directly — the blocking happens before theinspect.isawaitable()check on the next line can make any difference._function_executor.pyworkflow module already demonstrates a working pattern for detecting sync vs async functions and offloading sync ones to threads.agent-framework: 1.3.0.Acceptance Criteria
@tooldo not block the async event loop during execution — concurrent async tasks (including polling) continue to make progress while a sync tool runsResponsesAgentServerHost