Quick Start
1
From a sync tool
Use The user runs sync code that needs async I/O; the bridge executes coroutines without nested event loops.
run_coroutine_from_any_context to call async code from a sync tool:2
From an async tool
Use
run_sync_in_executor to call blocking code from an async tool without blocking the event loop:3
Detecting the context
Use
is_async_context to create dual-mode helpers:How It Works
The bridge probes for a running event loop usingasyncio.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.
Configuration Options
Common Patterns
Reusing async SDKs from sync tools
Offloading blocking calls from async tools
Context-aware dual-mode helper
Best Practices
Prefer await when you're already async
Prefer await when you're already async
Calling
run_coroutine_from_any_context inside an async def raises RuntimeError by design. If you’re in a coroutine, use await instead:Don't wrap everything
Don't wrap everything
Only wrap at the true sync/async boundary. Avoid creating unnecessary bridge calls in the middle of your call stack:
Set a sensible timeout
Set a sensible timeout
The default 300 seconds is large for most use cases. Tighten for latency-critical tools:
Check is_async_context() for dual-mode helpers
Check is_async_context() for dual-mode helpers
When building utilities that work in both sync and async contexts, check the context first:
Used by
The following synchronous APIs route throughrun_sync() and therefore honour PRAISONAI_RUN_SYNC_TIMEOUT consistently:
praisonai.bots.WebhookApproval.request_approval_sync()praisonai.bots.HTTPApproval.request_approval_sync()praisonai.integrations.get_available_integrations()praisonai._run_praisonai(added PR #1681) — boots the InteractiveRuntime on the persistent background loop. If you callPraisonAI.run()from inside a running event loop, you now get a clearRuntimeErrorinstead of a silent deadlock.- All ~77 wrapper-side
run_synccall sites (gateway, a2u, mcp_server, scheduler) — see PR #1583 for the full list. praisonai.auto.BaseAutoGenerator._structured_completion/_run_coro_syncrunner threads (added in the fix for #3340) — inherit the caller’sscoped_bridge()viacontextvars.copy_context().praisonai_code.cli.features.agent_tools._run_sync(added via PR #3361) — a separate, module-local bridge in the Tier-2praisonai-codepackage that ACP/LSP agent-centric tools route through. Does not importpraisonai._async_bridge(C7 gate), but reads the samePRAISONAI_RUN_SYNC_TIMEOUTenv var and enforces the same 300 s default. Safe under a running loop (offloads to aThreadPoolExecutor), and returns promptly on timeout without waiting for the abandoned worker.
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 the module-level shutdown() before any
run_sync() is a safe no-op — it only affects the shared default bridge, not any AsyncBridge() instances you create yourself.Troubleshooting
RuntimeError: run_coroutine_from_any_context() cannot be called from async context
You’re trying to use the bridge inside a coroutine. Useawait instead:
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.
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.
TimeoutError: run_sync_or_offload() worker did not complete within s
The offloaded coroutine did not finish insidetimeout + 1s. Check for blocking I/O or a missing await, or raise PRAISONAI_RUN_SYNC_TIMEOUT if the work is legitimately long-running.
PermissionError in approval system
The approval system now fails fast in async contexts. Configure a non-console backend:Wrapper Bridge (praisonai._async_bridge)
The wrapper layer provides a module-level run_sync() for CLI scripts and single-tenant servers, plus a public AsyncBridge class when you need an isolated loop per tenant or service.
- Module-level (default)
- Per-instance AsyncBridge
When to use a per-instance bridge
API Reference:
Environment:
PRAISONAI_RUN_SYNC_TIMEOUT: Default timeout in seconds (300). Read by bothpraisonai._async_bridge(this page) andpraisonai_code.cli.features.agent_tools._run_sync(see Agent-Centric Tools → Timeouts & Cancellation).
- CLI approval protocol (ACP/LSP tools)
- Interactive runtime start/stop operations
- Deployment scheduler
- Gateway operations
run_sync_or_offload — safe from any calling context
Use run_sync_or_offload() when your code path can be reached from either a normal script or from inside a running event loop (FastAPI, Jupyter, async tests) — one helper, no RuntimeError.
1
Import and call
2
Inside a FastAPI handler
Same helper, no code change — it just works:
Configuration Options
Called from a plain sync caller, it dispatches to the active
AsyncBridge via run_sync, sharing its background loop and connection pools. Called from inside a running loop, it copies the caller’s ContextVars onto a worker thread that hands the coroutine to the same bridge — never a fresh asyncio.new_event_loop() — so a caller-installed scoped_bridge() binding still wins and LiteLLM/HTTPX per-loop connection pools are preserved. Exceptions re-raise on the caller thread.
How it differs from run_sync
run_sync_or_offload() now bounds thread.join() at timeout + 1.0s. If the worker is still alive it raises TimeoutError("run_sync_or_offload() worker did not complete within {timeout}s"). The extra second covers thread hand-off/teardown so the join does not spuriously time out before the worker records its own error.Best Practices
Prefer run_sync_or_offload for any 3-way surface (CLI + YAML + Python)
Prefer run_sync_or_offload for any 3-way surface (CLI + YAML + Python)
The caller’s context isn’t knowable at write time — the same code path can be reached from a plain script, a YAML run, or a Python call inside a running loop.
run_sync_or_offload handles all three without a RuntimeError, while still reusing the shared bridge and its connection pools.Keep run_sync for sync-only paths
Keep run_sync for sync-only paths
run_sync fails loudly inside a loop, which is what you want for a code path that must never be called from one — the RuntimeError surfaces an architectural bug instead of silently offloading.praisonai.auto, persistence.orchestrator, api.agent_invoke) are migrated onto it.
Related
run_sync
Module-level runner for sync-only paths.
scoped_bridge
Per-session bridge that
run_sync_or_offload preserves across the offload hop.Agent-Centric Tools
The Tier-2
_run_sync (PR #3361) mirrors the same design.arun_sync_or_offload — await from an async context
Use arun_sync_or_offload() from async callers (FastAPI/Starlette handlers, Jupyter cells, async tests) — await it instead of parking the loop thread with the sync helper.
1
Import and await
2
Inside a FastAPI handler
await, don’t park — the loop stays responsive while the agent runs:Configuration Options
Unlike
run_sync_or_offload, this variant does not park the running loop thread. It submits coro to the active AsyncBridge and awaits the result, so the loop keeps serving other work. The coroutine runs on the shared background bridge loop — never a fresh asyncio.new_event_loop() — so a caller-installed scoped_bridge() binding still wins and per-loop LiteLLM/HTTPX connection pools are preserved. On timeout or cancellation, the in-flight future is cancelled before the exception re-raises.
Best Practices
Await it from async handlers, not run_sync_or_offload
Await it from async handlers, not run_sync_or_offload
Inside a running loop,
run_sync_or_offload offloads onto a worker thread and blocks the caller until the coroutine finishes. arun_sync_or_offload awaits instead, so the loop keeps serving other requests.Keep the sync helper for sync-only paths
Keep the sync helper for sync-only paths
Plain scripts and CLI entry points can’t
await. Use run_sync_or_offload (or run_sync) there and reserve arun_sync_or_offload for code already inside a coroutine.Related
run_sync_or_offload
Sync sibling for non-async callers.
scoped_bridge
Per-session bridge preserved across the await.
Per-Session Scoped Bridge
Servers and gateways that handle multiple concurrent sessions need each session to run on its own loop+thread binding.current_bridge() and scoped_bridge() provide ContextVar-backed per-session isolation so sessions never share a bridge accidentally.
When to use scoped bridges
Usescoped_bridge() inside any request handler that may run concurrently with other handlers — for example a FastAPI endpoint, a Starlette WebSocket handler, or a custom bot session dispatcher.
The isolation now extends into sync-completion runner threads used by AutoGenerator.generate() — a scoped_bridge() set on the caller is honoured inside the worker thread via contextvars.copy_context().
scoped_bridge() context manager
ContextVar so nested scopes work correctly in async tasks and threads — each concurrent task sees only its own bridge.
Scope-owned bridge (preferred)
scoped_bridge() creates a fresh bridge and tears it down with permanent=True when the with block ends. Internal code that calls module-level run_sync() inside the block uses the scoped bridge via contextvars — no import changes required.
current_bridge() for introspection
current_bridge() returns the bridge bound to the current async task, or None when no scope is active. Use it to inspect which bridge is in use without passing it explicitly through call stacks.
Multi-session server example
API Reference
Related
Async Agents Guide
Thread Safety & Concurrency

