Skip to content

Python: run sync tools off the event loop - #5773

Merged
Evan Mattson (moonbox3) merged 2 commits into
microsoft:mainfrom
he-yufeng:fix/sync-tool-offload
Jun 4, 2026
Merged

Python: run sync tools off the event loop#5773
Evan Mattson (moonbox3) merged 2 commits into
microsoft:mainfrom
he-yufeng:fix/sync-tool-offload

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

Summary

  • run synchronous Python tools in a worker thread when invoked through the async FunctionTool.invoke path
  • keep async tools on the event loop and still await sync wrappers that return awaitables
  • add a regression test that proves a blocking sync tool no longer prevents another coroutine from running

Fixes #5741.

To verify

  • uv run pytest tests/core/test_tools.py -q
  • uv run ruff check agent_framework tests/core/test_tools.py
  • uv run ruff format --check agent_framework tests/core/test_tools.py
  • uv run pyright
  • uv run mypy --config-file pyproject.toml agent_framework
  • python -m py_compile agent_framework\_tools.py tests\core\test_tools.py

Copilot AI review requested due to automatic review settings May 12, 2026 08:39
@moonbox3 Evan Mattson (moonbox3) added the python Usage: [Issues, PRs], Target: Python label May 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python FunctionTool.invoke async execution path so that synchronous tools run in a worker thread (avoiding event-loop stalls), while async tools continue to run on the event loop. It also adds a regression test to ensure a blocking sync tool no longer prevents other coroutines from running (fixing #5741).

Changes:

  • Add an internal async helper to route async tools to the event loop and sync tools to asyncio.to_thread().
  • Update FunctionTool.invoke to use the helper in both the observability-enabled and disabled code paths.
  • Add an asyncio/threading regression test to confirm sync tool invocation does not block the event loop.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/core/agent_framework/_tools.py Offloads synchronous tool execution to a worker thread during async invocation via a new helper.
python/packages/core/tests/core/test_tools.py Adds a regression test validating that sync tools no longer block other event-loop tasks.

Comment thread python/packages/core/agent_framework/_tools.py
@moonbox3

Evan Mattson (moonbox3) commented May 18, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py10257692%219–220, 397, 399, 412, 437–439, 447, 465, 479, 486, 493, 516, 518, 525, 533, 662, 696–698, 701–703, 705, 711, 762–764, 789, 815, 819, 857–859, 863, 885, 1028–1029, 1033, 1069, 1081, 1088–1091, 1112, 1116, 1120, 1134–1136, 1486, 1578, 1606, 1628, 1636, 1770, 1774, 1820, 1881–1882, 1985, 2038, 2058, 2060, 2116, 2179, 2372, 2435–2436, 2574–2575, 2642, 2647, 2654
packages/core/agent_framework/_harness
   _background_agents.py2582092%173, 198–199, 224–226, 229–230, 233–234, 374, 419, 422, 447, 456, 460, 519–522
TOTAL37988441388% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7579 34 💤 0 ❌ 0 🔥 2m 4s ⏱️

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Rebased this branch onto current upstream/main.

Validation:

python -m pytest python\packages\core\tests\core\test_tools.py -q --basetemp .tmp\pytest-5773
python -m py_compile python\packages\core\agent_framework\_tools.py python\packages\core\tests\core\test_tools.py
git diff --check

@eavanvalkenburg

Copy link
Copy Markdown
Member

Yufeng He (@he-yufeng) please check the failing tests

auto-merge was automatically disabled June 2, 2026 14:30

Head branch was pushed to by a user without write access

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Rebased this branch onto current upstream/main after the failing-test ping.

Validation on the rebased head:

uv run --dev python -m pytest packages\core\tests\core\test_tools.py::test_invoke_sync_tool_does_not_block_event_loop packages\core\tests\core\test_tools.py::test_invoke_skip_parsing_awaits_async_functions -q --basetemp .tmp\pytest-5773-target
uv run --dev python -m pytest packages\core\tests\core\test_tools.py -q --basetemp .tmp\pytest-5773-full2
uv run --dev python -m ruff check packages\core\agent_framework\_tools.py packages\core\tests\core\test_tools.py
uv run --dev python -m py_compile packages\core\agent_framework\_tools.py packages\core\tests\core\test_tools.py
git diff --check upstream/main..HEAD

@TaoChenOSU

Copy link
Copy Markdown
Contributor

Yufeng He (@he-yufeng) Unit tests are still failing. Please have another look, thanks!

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Thanks for the heads-up. The failing Python tests were coming from the background-agent tools: background_agents_start_task and background_agents_continue_task call asyncio.create_task(...), so running them through the new sync-tool thread offload left them without a running event loop.

I updated the branch so normal sync tools still run off the event loop by default, while these two event-loop-dependent harness tools opt into running on the current loop. I also rebased the branch onto current upstream/main.

Validated locally:

uv run --dev python -m ruff check packages/core/agent_framework/_tools.py packages/core/agent_framework/_harness/_background_agents.py packages/core/tests/core/test_tools.py packages/core/tests/core/test_harness_background_agents.py
uv run --dev python -m py_compile packages/core/agent_framework/_tools.py packages/core/agent_framework/_harness/_background_agents.py packages/core/tests/core/test_tools.py packages/core/tests/core/test_harness_background_agents.py
uv run --dev python -m mypy --config-file pyproject.toml packages/core/agent_framework/_tools.py packages/core/agent_framework/_harness/_background_agents.py
uv run --dev python -m pytest packages\core\tests\core\test_tools.py packages\core\tests\core\test_harness_background_agents.py -q --basetemp .tmp\pytest-5773-after-rebase

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Rebased this branch onto current upstream/main again and reran the focused checks after the failing-test pings.

Validation on Windows:

uv run --dev python -m pytest packages\core\tests\core\test_tools.py packages\core\tests\core\test_harness_background_agents.py -q --basetemp .tmp\pytest-5773-20260604
uv run --dev python -m ruff check packages\core\agent_framework\_tools.py packages\core\agent_framework\_harness\_background_agents.py packages\core\tests\core\test_tools.py packages\core\tests\core\test_harness_background_agents.py
uv run --dev python -m py_compile packages\core\agent_framework\_tools.py packages\core\agent_framework\_harness\_background_agents.py packages\core\tests\core\test_tools.py packages\core\tests\core\test_harness_background_agents.py
uv run --dev python -m mypy --config-file pyproject.toml packages\core\agent_framework\_tools.py packages\core\agent_framework\_harness\_background_agents.py
git diff --check upstream/main..HEAD

The targeted test_tools.py and test_harness_background_agents.py set passes on the rebased head.

@moonbox3

Copy link
Copy Markdown
Contributor

Yufeng He (@he-yufeng)

FAILED: pyright in packages/core
Poe => pyright
/home/runner/work/agent-framework/agent-framework/python/packages/core/agent_fra
mework/_harness/_background_agents.py
/home/runner/work/agent-framework/agent-framework/python/packages/core/agent_f
ramework/_harness/_background_agents.py:352:38 - error:
"_invoke_sync_on_event_loop" is protected and used outside of the class in which
it is declared (reportPrivateUsage)
/home/runner/work/agent-framework/agent-framework/python/packages/core/agent_f
ramework/_harness/_background_agents.py:476:41 - error:
"_invoke_sync_on_event_loop" is protected and used outside of the class in which
it is declared (reportPrivateUsage)
2 errors, 0 warnings, 0 informations

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Pushed a small follow-up for the package-check failure.

Root cause: package-wide pyright reports the dynamic _invoke_sync_on_event_loop marker as private usage. The marker is intentional for these two harness tools, so this now uses a local pyright suppression at the assignment site instead of changing the runtime behavior.

Validation run locally:

  • uv run --dev pyright packages\core\agent_framework\_harness\_background_agents.py: 0 errors
  • uv run --dev ruff check packages\core\agent_framework\_harness\_background_agents.py: passed
  • uv run --dev ruff format --check packages\core\agent_framework\_harness\_background_agents.py: passed
  • uv run --dev mypy packages\core\agent_framework\_harness\_background_agents.py: passed
  • uv run --dev python -m pytest packages\core\tests\core\test_tools.py packages\core\tests\core\test_harness_background_agents.py -q --basetemp .tmp\pytest-5773-20260604c: passed
  • git diff --check: passed

The Windows full-test jobs also reported a _native_wasm::WasmSandbox unraisable cleanup error near the end of the complete suite; that appears outside this PR's touched harness-tool path, so I left the code change scoped to the pyright package-check failure.

@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Jun 4, 2026
Merged via the queue into microsoft:main with commit f29bae8 Jun 4, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants