Skip to content

Async correctness: asyncio.run() called from sync context reachable by async callers #1165

@MervinPraison

Description

@MervinPraison

Summary

The tool approval path in Agent calls asyncio.run() from a synchronous method that is reachable from async execution contexts. This causes a hard RuntimeError when an async workflow triggers tool approval.

Problem

agent.py:5064-5067:

if hasattr(backend, 'request_approval_sync'):
    decision = backend.request_approval_sync(request)
else:
    decision = asyncio.run(backend.request_approval(request))

This is inside _check_tool_approval_sync(), which is called from the synchronous _execute_tool_impl(). However, _execute_tool_impl() is callable from async agent workflows — when an async agent executes a tool, it eventually calls into this sync path.

When an event loop is already running (which it always is in async workflows), asyncio.run() raises:

RuntimeError: asyncio.run() cannot be called from a running event loop

Why this matters

The philosophy states: "Multi-agent + async safe by default". Any approval backend that only implements the async request_approval() method (not the optional request_approval_sync()) will crash when used from an async agent workflow. This is a silent correctness trap — it works in sync-only usage and fails only in production async deployments.

The deeper pattern issue

This is not an isolated case. The codebase has a general pattern of sync methods that assume they're never called from an async context. The asyncio.run() fallback suggests awareness of the async path, but the fix is incomplete — it should detect a running loop and use loop.run_until_complete() or an async-native path instead.

Impact

  • Hard crash in async multi-agent workflows when tool approval backends lack request_approval_sync()
  • Works fine in testing (sync), fails in production (async) — the worst kind of bug
  • Violates: "Multi-agent + async safe by default"

Suggested Fix

  1. Detect running event loop before choosing sync vs async path:
try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    loop = None

if loop is not None:
    # We're in an async context — schedule coroutine properly
    import concurrent.futures
    with concurrent.futures.ThreadPoolExecutor() as pool:
        decision = loop.run_in_executor(pool, lambda: asyncio.run(backend.request_approval(request)))
else:
    decision = asyncio.run(backend.request_approval(request))
  1. Or better: require all approval backends to implement request_approval_sync() and make it part of the protocol, removing the asyncio.run() fallback entirely.

  2. Audit for other asyncio.run() calls in sync methods reachable from async contexts.

Files

  • src/praisonai-agents/praisonaiagents/agent/agent.py (lines 5064-5070)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysis

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions