from praisonaiagents.utils.async_bridge import is_async_context, run_coroutine_from_any_contextimport httpxasync def _async_fetch(url: str) -> str: async with httpx.AsyncClient() as client: return (await client.get(url)).textdef smart_fetch(url: str) -> str: """Context-aware fetch that works in both sync and async.""" if is_async_context(): raise RuntimeError("Use await smart_fetch_async(url) in async context") return run_coroutine_from_any_context(_async_fetch(url))async def smart_fetch_async(url: str) -> str: """Async version for use in async contexts.""" return await _async_fetch(url)
The bridge probes for a running event loop using asyncio.get_running_loop(). If no loop exists, it safely creates one with asyncio.run(). If a loop is already running, it raises RuntimeError to prevent deadlocks.
Calling run_coroutine_from_any_context inside an async def raises RuntimeError by design. If you’re in a coroutine, use await instead:
# Goodasync def my_async_tool(): result = await my_coroutine()# Bad - will raise RuntimeErrorasync def my_async_tool(): result = run_coroutine_from_any_context(my_coroutine())
Don't wrap everything
Only wrap at the true sync/async boundary. Avoid creating unnecessary bridge calls in the middle of your call stack:
# Good - bridge at the boundarydef sync_tool(): return run_coroutine_from_any_context(async_logic())# Bad - unnecessary nestingdef sync_tool(): def inner(): return run_coroutine_from_any_context(async_logic()) return inner()
Set a sensible timeout
The default 300 seconds is large for most use cases. Tighten for latency-critical tools:
# Good for quick operationsresult = run_coroutine_from_any_context(quick_api_call(), timeout=10)# Good for long operationsresult = run_coroutine_from_any_context(model_training(), timeout=3600)
Check is_async_context() for dual-mode helpers
When building utilities that work in both sync and async contexts, check the context first:
def smart_helper(): if is_async_context(): raise RuntimeError("Use await smart_helper_async() in async context") return run_coroutine_from_any_context(async_implementation())async def smart_helper_async(): return await async_implementation()
All ~77 wrapper-side run_sync call sites (gateway, a2u, mcp_server, scheduler) — see PR #1583 for the full list.
These sync wrappers now raise RuntimeError("run_sync() cannot be called from a running event loop; await the coroutine directly instead.") when called from inside an active asyncio loop. Previously they would silently spawn a worker thread. If you call any of these from async code, switch to await request_approval(...) (or the equivalent async method) directly. This is a deliberate fail-fast change — the silent thread spawn was masking architectural bugs in multi-agent setups.
asyncio.run() cannot be called from a running event loop
This error used to leak from SDK internals before the async bridge was implemented. If you see this on current versions, upgrade to the latest release.
The approval system now fails fast in async contexts. Configure a non-console backend:
from praisonaiagents.approval import get_approval_registry, WebhookBackend# Configure for async compatibilityget_approval_registry().set_backend(WebhookBackend(url="http://localhost:8080/approve"))