Skip to content

fix: prevent 'event loop already running' when async tools run in parallel#2207

Merged
teknium1 merged 1 commit intomainfrom
hermes/hermes-3369cdb1
Mar 20, 2026
Merged

fix: prevent 'event loop already running' when async tools run in parallel#2207
teknium1 merged 1 commit intomainfrom
hermes/hermes-3369cdb1

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

Summary

Fixes RuntimeError: This event loop is already running when async tools (like web_extract) are executed as part of a parallel tool batch.

Root cause

When the model returns multiple tool calls, run_agent.py runs them concurrently in a ThreadPoolExecutor (line 4598). Each worker thread calls _run_async() which used a shared persistent event loop (_get_tool_loop()). If two async tools ran in parallel, the second thread would call tool_loop.run_until_complete() while the first thread was still using it → RuntimeError: This event loop is already running.

This only happened when:

  • Multiple tool calls returned by the model
  • At least two are async tools (web_extract, web_search, etc.)
  • They get parallelized (different scopes, no file conflicts)

It didn't reproduce in CLI single-tool-at-a-time usage, only in parallel execution.

Fix

Detect worker threads (threading.current_thread() is not threading.main_thread()) and use asyncio.run() with a per-thread fresh loop instead of the shared persistent one. The shared loop is still used for the main thread (CLI sequential path) to keep cached async clients alive.

Test plan

5647 passed, 200 skipped, 23 deselected
…allel

When the model returns multiple tool calls, run_agent.py executes them
concurrently in a ThreadPoolExecutor. Each thread called _run_async()
which used a shared persistent event loop (_get_tool_loop()). If two
async tools (like web_extract) ran in parallel, the second thread would
hit 'This event loop is already running' on the shared loop.

Fix: detect worker threads (not main thread) and use asyncio.run() with
a per-thread fresh loop instead of the shared persistent one. The shared
loop is still used for the main thread (CLI sequential path) to keep
cached async clients (httpx/AsyncOpenAI) alive.
@teknium1 teknium1 merged commit aafe86d into main Mar 20, 2026
1 check passed
teknium1 added a commit that referenced this pull request Mar 20, 2026
…gate

Completes the event loop lifecycle fix trilogy (#2190#2207#2214). Per-thread persistent loops for worker threads prevent GC crashes on cached async clients.
outsourc-e pushed a commit to outsourc-e/hermes-agent that referenced this pull request Mar 26, 2026
…allel (NousResearch#2207)

When the model returns multiple tool calls, run_agent.py executes them
concurrently in a ThreadPoolExecutor. Each thread called _run_async()
which used a shared persistent event loop (_get_tool_loop()). If two
async tools (like web_extract) ran in parallel, the second thread would
hit 'This event loop is already running' on the shared loop.

Fix: detect worker threads (not main thread) and use asyncio.run() with
a per-thread fresh loop instead of the shared persistent one. The shared
loop is still used for the main thread (CLI sequential path) to keep
cached async clients (httpx/AsyncOpenAI) alive.

Co-authored-by: Test <test@test.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant