Skip to content

Python: run sync tools off the event loop#5773

Merged
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
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

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

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 he-yufeng force-pushed the fix/sync-tool-offload branch from 2d983ec to 37330b7 Compare May 21, 2026 05:13
@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

@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 he-yufeng force-pushed the fix/sync-tool-offload branch from 7a0e06e to 42ff4ae Compare June 2, 2026 14:30
@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 TaoChenOSU enabled auto-merge June 2, 2026 17:45
@TaoChenOSU TaoChenOSU disabled auto-merge June 2, 2026 17:50
@TaoChenOSU

Copy link
Copy Markdown
Contributor

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

@he-yufeng he-yufeng force-pushed the fix/sync-tool-offload branch from 42ff4ae to 8d4af1c Compare June 2, 2026 18:11
@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.

@he-yufeng he-yufeng force-pushed the fix/sync-tool-offload branch from 8d4af1c to e30c431 Compare June 4, 2026 04:10
@moonbox3

moonbox3 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@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 moonbox3 enabled auto-merge June 4, 2026 04:37
@moonbox3 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

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