Skip to main content
Async DB hooks enable non-blocking database operations in async agents through automatic async/sync detection and asyncio.to_thread fallback.
Breaking Change in PR #1829: The async_* prefixed methods have been removed from async stores. The orchestrator now uses isinstance(store, AsyncConversationStore) for dispatch instead of runtime method introspection.
The user chats asynchronously; DB hooks persist messages without blocking the event loop.

Quick Start

1

Async Context Manager

Use async DB operations with the async context manager for automatic cleanup.
2

Wire to Async Agent

Connect async DB hooks to an async agent for seamless persistence.

How It Works

Shared dispatch for both entry points

Both persistence entry points flow through the same async-store dispatch. As of PR #4394, the async-store dispatch (isinstance(store, AsyncConversationStore)await store.method(...) vs. asyncio.to_thread(store.method, ...)) lives on PraisonAIDB, in its internal _call_store / _dispatch_async helpers. PersistenceOrchestrator.on_message / aon_message / on_agent_end / aon_agent_end delegate to those helpers instead of keeping a second copy, so the async/sync-store behaviour is identical no matter which entry point you pick — MemoryConfig(db=PraisonAIDB(...)) or the orchestrator-based wrap_agent_with_persistence / PersistentAgent / create_persistent_session.
_call_store splits by op kind through the shared bridge helper praisonai._async_bridge.dispatch_maybe_awaitable: writes stay fire-and-forget on the bridge (idempotent, uuid-keyed); reads (_READ_OPS = {"get", "get_session", "get_messages", "list_sessions"}) route through run_sync_or_offload, returning the real value on a sync path and raising loudly inside a running loop. Read-vs-write intent is passed explicitly as kind=DispatchKind.READ|WRITE at the call site (PR #5038); _READ_OPS is retained as the backward-compatible default classifier _call_store uses to pick the kind from the op’s name. The same helper backs _merge_and_set (used by all four sync completion hooks) and on_agent_end. As of PR #4861, _call_store and flush_pending_writes are instance methods on PraisonAIDB (previously staticmethods), and each instance tracks its own fire-and-forget writes in self._bg_writes — not a process-global set. _call_store, _dispatch_async, _merge_and_set, and _READ_OPS are internal plumbing on PraisonAIDB, not public API; they are named here only to describe the shared dispatch mechanism. DispatchKind and dispatch_maybe_awaitable in praisonai._async_bridge are public — see Async Bridge → dispatch_maybe_awaitable.
Because bg-write tracking is per-instance (PR #4861), a tenant’s close() / aclose() flushes only its own in-flight writes — one tenant’s stuck backend can no longer consume another tenant’s 5-second flush budget or discard its writes. See Thread Safety.

Sync reads from inside a running loop

Bare sync reads reached from inside a running event loop fail loudly instead of silently dropping the read.
Bare sync reads from inside a running loop fail loudly. Calling a sync hook whose store op is a bare read — get, get_session, get_messages, or list_sessions — from inside a running event loop raises RuntimeError steering you to the async surface. In previous releases it silently returned None: a resumed session looked empty. Outside a loop, sync reads resolve the coroutine transparently. Writes are unaffected — they remain uuid-keyed fire-and-forget on the bridge.Fixed in PR #4821, fixes #4820.
The four sync completion hooks — on_run_end, on_agent_end, on_trace_end, on_span_end — read internally (a read-modify-write) but do not fail loudly from a running loop. They persist correctly via the async completion write path below. See PR #4948.

Async completion write path

The four sync completion hooks persist correctly from inside a running loop by routing the whole read-modify-write through one bridge submission. on_run_end, on_agent_end, on_trace_end, and on_span_end each call _merge_and_set, which builds a single coroutine that does get → merge → set on the state store. Since PR #5038, _merge_and_set hands that coroutine to dispatch_maybe_awaitable(..., kind=DispatchKind.WRITE, tracker=self._bg_writes, tracker_lock=self._bg_writes_lock, op_name=f"merge_and_set:{key}") instead of hand-rolling the submit/track/callback — same behaviour, one path. It becomes a tracked fire-and-forget write (recorded in self._bg_writes). Because the read never crosses the loop boundary, the write half always executes — the completion actually persists instead of leaving the record stuck at status="running". The merge is uuid-keyed and idempotent, so deferring the write is safe by construction. Pre-existing fields (run_id, started_at, input_content, spans, events) are preserved; completion fields (output_content, status, ended_at) are merged in. Ordering guarantee. Callers who must see the persisted record before returning (tests, request handlers reading the state store synchronously) should flush the pending write first.
Only completion hooks that read internally changed. Bare reads (get, get_session, get_messages, list_sessions) still fail loudly from inside a running loop — PR #4821 semantics are unchanged.

Which hook to call from where

Pick sync or async by whether you sit inside a running loop and whether you read or write.
Follow-up note on completion hooks. The diagram’s “bare read” branch covers get, get_session, get_messages, and list_sessions. The completion hooks (on_run_end / on_agent_end / on_trace_end / on_span_end) read internally but take the safe “fire-and-forget on bridge” branch via _merge_and_set — see Async completion write path.
Troubleshooting. Symptom in older versions: a resumed session came back empty even though messages had been persisted; a completed run/trace/span record lost run_id, started_at, input_content, or its spans/events after a sync completion hook fired from async code — the record stayed status="running" with no output_content or ended_at. Root cause: the internal read reached from a running loop was dropped and the merge overwrote (or never wrote) the record. Fixed in PR #4948 (fixes #4945) — the sync completion hooks now route the whole read-modify-write through one bridge submission, so the completion actually persists from inside a running loop. Bare reads still raise RuntimeError pointing you at the aon_* hook (PR #4821).

State-store lifecycle key

The wrapper writes both start and end under agent:{session_id}, so the end transition updates the same record the start wrote instead of a disconnected one. Start previously used agent:{session_id}:{agent_id or name} and end wrote a separate record. Consumers who query the state store directly must update their key format to agent:{session_id}.

Configuration Options

Signature change in PR #3857: aon_agent_start(agent_name, session_id, user_id, metadata) -> List and aon_agent_end(session_id, metadata). user_id is now persisted (previously silently dropped) and aon_agent_start returns the resumed message list. Update any custom AsyncDbAdapter implementations to match.
All async hooks support these signatures from the DB adapter:

Common Patterns

Complete Async Lifecycle

Sync Store Compatibility

Native Async Store


Best Practices

Manual aclose() is now safe and idempotent — calling it twice does nothing harmful. Prefer async with for exception-safety and readability, so cleanup runs even when an error is raised mid-block:
close() and aclose() reset the internal stores, so re-entering a with db: ... block after close cleanly re-initializes instead of dispatching to closed handles:
For high-throughput async applications, implement native async methods:
All hooks accept optional metadata dictionaries:
Both sync and async context managers are supported:

Persistence Overview

Complete persistence system documentation

Agent Architecture

Learn about async agent patterns