Background Workers
By default, belief tracking runs as asyncio background tasks in the same event loop as your application. For production deployments that require task durability (surviving process restarts, guaranteed delivery, retry queues), you can offload tracking to Celery or RQ distributed workers.
Dispatcher Comparison
| Dispatcher | Durability | Setup | Best For |
|---|---|---|---|
asyncio | ❌ Lost on crash | Zero — built-in | Default. Single-process apps, development, low-stakes tracking. |
sync | ✅ Inline (blocking) | Zero — built-in | Debugging, unit testing, synchronous applications. |
celery | ✅ Persisted in broker | Redis/RabbitMQ + workers | Production. Task retry, monitoring (Flower), multi-worker. |
rq | ✅ Persisted in Redis | Redis + workers | Simpler alternative to Celery. Fewer features, easier setup. |
Celery Dispatcher
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
celery_app | Celery | (required) | A configured Celery application instance. Must have a broker configured (Redis or RabbitMQ). The dispatcher registers a Celery task that executes the tracking pipeline. |
pythonfrom celery import Celery
from beliefstate import BeliefTracker, TrackerConfig
from beliefstate.dispatcher import CeleryDispatcher
celery_app = Celery("tasks", broker="redis://localhost:6379/0")
tracker = BeliefTracker(
config=TrackerConfig(),
adapter=app_adapter,
dispatcher=CeleryDispatcher(celery_app=celery_app)
)
RQ (Redis Queue) Dispatcher
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
queue | rq.Queue | (required) | A configured RQ Queue instance. Must be connected to a Redis server. The dispatcher enqueues tracking tasks onto this queue for worker processing. |
pythonfrom redis import Redis
from rq import Queue
from beliefstate.dispatcher import RQDispatcher
queue = Queue("belief-tasks", connection=Redis())
tracker = BeliefTracker(
config=TrackerConfig(),
adapter=app_adapter,
dispatcher=RQDispatcher(queue=queue)
)
Worker Setup
In your worker process, register the global tracker so tasks can execute:
worker_startup.pyfrom beliefstate.dispatcher import register_global_tracker
from my_app import tracker
# Register BEFORE processing any tasks
register_global_tracker(tracker)
⚠️ Context Propagation: ContextVar (session_context) does NOT propagate across process boundaries. The dispatchers explicitly serialize session_id into task payloads to ensure it reaches worker processes correctly.
Resilience
BeliefState wraps internal adapter calls with production-grade resilience patterns to handle API failures gracefully. These patterns ensure that transient provider outages don't silently drop beliefs or cascade failures into your application.
Retry with Exponential Backoff
All adapter calls (generate, embeddings) automatically retry transient failures with jittered exponential backoff. Configure via TrackerConfig (global) or per-adapter RetryConfig (adapter-level). Transient errors include rate limits (HTTP 429), server errors (5xx), timeouts, and connection errors. Permanent errors (invalid API key, malformed request) are raised immediately.
Circuit Breaker
When an LLM API goes down, the circuit breaker pattern prevents cascading failures by fast-failing requests instead of queuing retries indefinitely:
pythonconfig = TrackerConfig(
enable_circuit_breaker=True,
circuit_breaker_failure_threshold=5, # Trip after 5 consecutive failures
circuit_breaker_recovery_timeout=30.0, # Wait 30s before trying recovery
)
# Catch circuit breaker open state
from beliefstate import CircuitBreaker, CircuitBreakerOpenException
try:
result = await tracker.track(...)
except CircuitBreakerOpenException:
print("Circuit breaker is OPEN — failing fast")
ℹ️ Circuit Breaker State Machine:
- CLOSED (Normal): All requests flow through. If consecutive failures exceed
circuit_breaker_failure_threshold(default 5), the circuit trips to OPEN. - OPEN (Tripped): All requests immediately raise
CircuitBreakerOpenExceptionwithout hitting the LLM provider, saving rate limits and latency. - HALF-OPEN (Recovery probe): After
circuit_breaker_recovery_timeout(default 30s) elapses, the breaker allows a single probe request. If it succeeds → CLOSED. If it fails → back to OPEN for another cooldown cycle.
Health Checks
Use health checks to verify store and adapter connectivity at application startup or in readiness probes (Kubernetes /healthz endpoint):
python# Check full tracker health (store + adapter)
health = await tracker.health_check()
print(health) # {"store": True, "adapter": True}
# Check individual adapter
is_healthy = await adapter.health_check()
if not is_healthy:
logger.error("Provider is not responding — use fallback")
Observability
BeliefState includes built-in OpenTelemetry instrumentation for distributed tracing and metric collection, plus structured JSON logging for cloud log aggregators.
OpenTelemetry Setup
setup_otel() Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Master switch for OpenTelemetry instrumentation. Set to False to completely disable tracing and metrics collection (zero overhead). |
service_name | str | "beliefstate" | Service name reported in traces and metrics. Set this to your application's name to identify traces in your observability dashboard (e.g., "my-chatbot-api"). |
otel_exporter_otlp_endpoint | str | "http://localhost:4317" | OTLP gRPC endpoint for exporting traces and metrics. Point this to your collector (Jaeger, Grafana Tempo, Datadog Agent, etc.). |
pythonfrom beliefstate.observability import setup_otel
# Initialize OpenTelemetry tracing and metrics
setup_otel(
enabled=True,
service_name="my-app",
otel_exporter_otlp_endpoint="http://localhost:4317"
)
Distributed Tracing Spans
BeliefState creates hierarchical spans that trace the full lifecycle of each belief tracking task:
| Span Name | Description | Attributes |
|---|---|---|
beliefstate.extract_beliefs | Measures fact extraction prompt latency and token usage. | session_id, turn, beliefs_count, latency_ms |
beliefstate.detect_contradiction | Tracks time taken by semantic filters and NLI logic check. | session_id, candidates_checked, contradictions_found |
beliefstate.resolve_beliefs | Tracks store write times and resolution conflicts. | session_id, beliefs_added, beliefs_updated |
Exported Metrics
| Metric Name | Type | Description |
|---|---|---|
beliefstate.beliefs_extracted | Counter | Total number of facts parsed from conversation text. |
beliefstate.contradictions_detected | Counter | Total number of logical contradictions detected. |
beliefstate.beliefs_in_store | Gauge | Current number of active beliefs in the store. |
beliefstate.*_latency_ms | Histogram | Latency percentiles (p50, p90, p99) for database operations and provider calls. |
Structured Logging
All components emit structured JSON log events via the TrackerEvent dataclass. These events are designed for cloud log aggregators like Datadog, CloudWatch, and Loki.
TrackerEvent Fields
| Field | Type | Description |
|---|---|---|
session_id | str | The session ID this event relates to. |
operation | str | The pipeline stage: "extract_beliefs", "detect", "resolve", "store_write", etc. |
turn | int | The conversation turn number that triggered this event. |
latency_ms | float | Execution time in milliseconds for this operation. |
detail | str | Human-readable description of what happened (e.g., "Extracted 3 beliefs", "Found 1 contradiction"). |
log output{"session_id": "user_123", "operation": "extract_beliefs", "turn": 5, "detail": "Extracted 3 beliefs", "latency_ms": 142.5}
{"session_id": "user_123", "operation": "detect", "turn": 5, "detail": "Found 1 contradiction"}
{"session_id": "user_123", "operation": "resolve", "turn": 5, "detail": "Overwrote 1 belief", "latency_ms": 8.2}
pythonfrom beliefstate.logging_utils import TrackerEvent, log_event
# Emit a custom tracker event
event = TrackerEvent(
session_id="user_123",
operation="custom_check",
turn=5,
latency_ms=42.0,
detail="Ran custom validation"
)
log_event(event)
GDPR & Privacy
BeliefState supports full session deletion with auditable receipts for GDPR compliance (right to erasure, Article 17). The clear_session() method ensures complete data removal including draining any in-flight background tasks.
python# Delete all beliefs for a user (GDPR right to erasure)
receipt = await tracker.clear_session("user_123")
print(receipt)
# DeletionReceipt(
# session_id="user_123",
# beliefs_deleted=42,
# in_flight_tasks_drained=2,
# deleted_at=datetime(...)
# )
DeletionReceipt Fields
| Field | Type | Description |
|---|---|---|
session_id | str | The session ID whose data was deleted. |
beliefs_deleted | int | Number of belief records permanently removed from the store. |
in_flight_tasks_drained | int | Number of background tracking tasks that were cancelled or waited for completion before deletion. Ensures no new beliefs are written after the delete. |
deleted_at | datetime | UTC timestamp of when the deletion was executed. Serves as an auditable record for compliance. |
⚠️ Concurrency & Deletion Race Conditions: If a user requests account deletion while background belief extraction tasks are still running in the asyncio event loop, those tasks could complete and write new facts after the database is cleared.
To prevent this, clear_session() calls the dispatcher's drain_session(session_id) method first, which cancels or waits for all active tracking tasks for that session ID before executing the SQL or Redis delete commands.
Deletion Workflow
- Drain in-flight tasks: The dispatcher cancels or awaits all active tracking tasks for the session.
- Delete from store: All beliefs for the session are permanently removed from the store (SQLite DELETE, Redis DEL).
- Clean up locks: Session-specific async locks, turn counters, and provider tracking entries are removed from memory.
- Generate receipt: A
DeletionReceiptis returned with the count of deleted records and timestamp.
Best Practices
- Store receipts: Log or persist
DeletionReceiptobjects as audit evidence for GDPR compliance reviews. - Redis TTL: For Redis stores, combine
clear_session()withset_session_ttl()to auto-expire sessions that aren't explicitly deleted. - Celery/RQ workers: The drain mechanism works across distributed workers — it signals the task queue to cancel pending tasks for the session.