Quick Start
From a sync tool
run_coroutine_from_any_context to call async code from a sync tool:From an async tool
run_sync_in_executor to call blocking code from an async tool without blocking the event loop:Detecting the context
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
Timeout resolution: omitted vs None vs number
The timeout argument on run_sync, run_sync_or_offload, arun_sync_or_offload, and AsyncBridge.run_sync uses a private _UNSET sentinel as its default, so the bridge can tell “argument omitted” apart from an explicit timeout=None.
praisonai/integration/bridges/schedules_runner.py, praisonai/cli/commands/schedule.py) pass timeout=None so a long-running claimed job is never cancelled after 300 s.
_default_timeout() reads and parses PRAISONAI_RUN_SYNC_TIMEOUT per call, not at import. A malformed value (e.g. notanumber) falls back to 300.0 instead of crashing import praisonai, and a late-set value (dotenv loaded after import, per-request reconfig) takes effect on the next call.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
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
Set a sensible timeout
Set a sensible timeout
Check is_async_context() for dual-mode helpers
Check is_async_context() for dual-mode helpers
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.
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.The shared default’s atexit teardown hook is also registered lazily, on the first real use of the shared default bridge (inside AsyncBridge._spawn_locked(), guarded by self is globals().get("_BG")). A bare import praisonai no longer installs a process-wide atexit hook, so Django/Airflow/Streamlit embedders are unaffected until they actually call run_sync.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.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
This timeout branch is only reachable when the caller is inside a running loop and has opted intoPRAISONAI_ALLOW_LOOP_BLOCKING=true (the offload path). A plain-sync caller still hits the same TimeoutError via run_sync. The offloaded coroutine did not finish inside timeout + 1s — check for blocking I/O or a missing await, or raise PRAISONAI_RUN_SYNC_TIMEOUT if the work is legitimately long-running. Prefer migrating to await arun_sync_or_offload(...) / await praisonai.arun(...) so the loop is never blocked.
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
PRAISONAI_RUN_SYNC_TIMEOUT: Default timeout in seconds (300). Resolved per call (not at import) via_default_timeout(); a malformed value falls back to300.0, and late-set env vars (dotenv loaded after import, per-request reconfig) are honoured. An explicittimeout=Noneopts into an unbounded wait (used by the sync-scheduler bridges for long-lived jobs). 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
dispatch_maybe_awaitable — one helper for sync-hook dispatch
dispatch_maybe_awaitable() is the single owner of the “a sync call may return a coroutine; run it correctly” policy. It picks one of four branches from the running-loop state and an explicit DispatchKind.
DispatchKind makes read-vs-write intent explicit at the call site instead of depending on a name-string allow-list. The four branches:
PraisonAIDB._call_store, _merge_and_set, and on_agent_end paths route through — the split policy is no longer hand-rolled per call site (PR #5038). See Async DB Hooks → Shared dispatch for the db-adapter view.
dispatch_maybe_awaitable is a wrapper internal used by the db adapter — end users do not call it directly. The READ strict-inside-a-loop behaviour (PR #4821) and the completion-hook persistence guarantee (PR #4948) are pre-existing behaviours the helper now enforces in one place; PR #5038 is DRY consolidation with no user-visible behaviour change.praisonai/tests/unit/test_async_bridge.py::TestDispatchMaybeAwaitable pin the four branches:
test_non_awaitable_passthrough— a plain value is returned unchanged.test_no_loop_read_blocks_and_returns_value— no running loop +READblocks and returns the value.test_no_loop_write_blocks_and_returns_value— no running loop +WRITEruns to completion (fire-and-forget is a running-loop concern).test_running_loop_write_is_tracked_fire_and_forget— running loop +WRITEsubmits, tracks, and returnsNonewhile the write still completes.test_running_loop_failed_write_logs_and_does_not_raise— a failed deferredWRITEis swallowed (logged) via the done-callback, never surfacing to the sync caller.
run_sync_or_offload — strict inside a running loop
Use run_sync_or_offload() on a code path that can be reached from a plain script or from inside a running event loop (FastAPI, Jupyter, async tests). On a plain-sync caller it dispatches to run_sync. Inside a running loop it now raises RuntimeError by default (PR #4261) and steers you to the awaitable siblings — it never silently pins the loop.
Plain sync caller — works
Inside a FastAPI handler — await instead
RuntimeError. Make the handler async and await the coroutine directly:async def, await the awaitable sibling:Configuration Options
AsyncBridge via run_sync, sharing its background loop and connection pools. Called from inside a running loop with PRAISONAI_ALLOW_LOOP_BLOCKING=true, 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.
The strict RuntimeError
In the default (strict) mode, a call from inside a running loop raises with this message — grep for it in a traceback:
How it differs from run_sync
PRAISONAI_ALLOW_LOOP_BLOCKING opt-in, run_sync_or_offload() bounds thread.join() at timeout + 1.0s. If the worker is still alive it cancels the in-flight future and 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.Migration
Any caller that today relies on offload-inside-a-loop must either (a) migrate to the awaitable sibling (await praisonai.arun(...), await adapter.arun(...), or await arun_sync_or_offload(...)), or (b) set PRAISONAI_ALLOW_LOOP_BLOCKING=true as an interim measure. See PR #4261.
Best Practices
Prefer arun / arun_sync_or_offload inside a loop
Prefer arun / arun_sync_or_offload inside a loop
await the awaitable sibling so the loop stays responsive. The sync helper now raises RuntimeError by design rather than parking the loop.Keep run_sync_or_offload for plain-sync surfaces
Keep run_sync_or_offload for plain-sync surfaces
run_sync_or_offload dispatches to run_sync and reuses the shared bridge and its connection pools.Set PRAISONAI_ALLOW_LOOP_BLOCKING=true only for controlled migration windows
Set PRAISONAI_ALLOW_LOOP_BLOCKING=true only for controlled migration windows
timeout + 1s. Use it only as a bounded interim measure while migrating a known-safe path to the awaitable sibling — never as a long-term default.praisonai.auto, persistence.orchestrator, api.agent_invoke) are migrated onto it.
Related
run_sync
scoped_bridge
PRAISONAI_ALLOW_LOOP_BLOCKING=true.Agent-Centric Tools
_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.
Import and await
Inside a FastAPI handler
await, don’t park — the loop stays responsive while the agent runs:Configuration Options
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
run_sync_or_offload raises RuntimeError by default (and only offloads-and-blocks under PRAISONAI_ALLOW_LOOP_BLOCKING=true). 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
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
scoped_bridge
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
Sync generate_crew_and_kickoff auto-scoping
AgentsGenerator.generate_crew_and_kickoff() now wraps its adapter.run(...) call in scoped_bridge() automatically. Every sync run isolates its run_sync-driven work onto its own loop+thread, so a stuck coroutine in one agent/tenant can no longer park the shared default loop for the rest.
Callers doing multi-tenant sync runs no longer need to add their own scoped_bridge() around generate_crew_and_kickoff — a stuck coroutine in one run cannot affect another.
agenerate_crew_and_kickoff() (async) is intentionally not wrapped. Async callers awaiting adapter.arun(...) directly do not benefit from a per-call loop swap and get better connection-pool reuse without it.
