Skip to content

Python: [Bug]: Blocking synchronous tool execution freezes Responses API polling inside async event loop #5741

@lorenzo-caruso

Description

@lorenzo-caruso

Description

Hi,

I found an issue related to synchronous tool execution inside the Microsoft Agent Framework.

Problem

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:

1. The tool is implemented asynchronously

async def tool():
    await asyncio.sleep(120)

2. The synchronous tool is explicitly executed in a thread

await asyncio.to_thread(blocking_tool)

Proposed fix

Instead of executing the function first and checking afterward whether the result is awaitable, the framework should distinguish between sync and async functions before invocation.

Suggested implementation:

Instead of:

res = self.__call__(**call_kwargs)
result = await res if inspect.isawaitable(res) else res

Use:

if inspect.iscoroutinefunction(self.__call__):
    result = await self.__call__(**call_kwargs)
else:
    result = await asyncio.to_thread(self.__call__, **call_kwargs)

This would prevent blocking synchronous functions from freezing the event loop while preserving compatibility with async tools.


Expected behavior

  • 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

Thanks!

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework: 1.3.0

Python Version

No response

Additional Context

No response

Metadata

Metadata

Assignees

Labels

Type

No fields configured for Bug.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions