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()
praisonai._run_praisonai (added PR #1681) — boots the InteractiveRuntime on the persistent background loop. If you call PraisonAI.run() from inside a running event loop, you now get a clear RuntimeError instead of a silent deadlock.
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.PR #1692 — cancellation on timeout (May 2026). When a run_sync() call hits its timeout (default 300 s, or whatever PRAISONAI_RUN_SYNC_TIMEOUT is set to), the underlying coroutine is now actively cancelled on the background loop. The bridge waits up to 1 s for cancellation to propagate before re-raising TimeoutError. This means slow DB queries (SurrealDB, async MySQL), HTTP calls, and subprocess waits now release their connection / socket / pipe instead of leaking. Cancellation also fires on KeyboardInterrupt, SystemExit, and GeneratorExit.
The wrapper-layer bridge (praisonai._async_bridge) creates its
background loop lazily on the first run_sync() call. Pure imports
do not allocate a loop or thread. Calling shutdown() before any
run_sync() is a safe no-op.
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.
Symptom
Cause
Resolution
TimeoutError raised but you also see your coroutine’s finally: block run after the exception
Expected: cancellation propagated, cleanup ran.
No action needed; this is the new PR #1692 behaviour.
Test reference:praisonai/tests/unit/test_async_bridge.py::TestBridgeIntegration::test_timeout_cancels_coroutine_and_runs_finally — quote this in the page so users can verify the behaviour locally.
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"))
The wrapper layer has its own async bridge for PraisonAI-level operations:
from praisonai._async_bridge import run_sync, shutdown# Example: sync entry point calling async helperasync def async_helper(data: str) -> str: await asyncio.sleep(0.1) return f"Processed: {data}"def sync_entry_point(data: str) -> str: """Sync function that needs to call async code.""" return run_sync(async_helper(data), timeout=60)# For long-running serversimport atexitatexit.register(shutdown)
API Reference:
Function
Signature
Description
run_sync
(coro, *, timeout=300) -> T
Run coroutine on background loop
shutdown
() -> None
Clean shutdown of background loop
Environment:
PRAISONAI_RUN_SYNC_TIMEOUT: Default timeout in seconds (300)
Do not call run_sync from inside async def — use await instead. The function raises RuntimeError if called from within a running event loop to prevent deadlocks.