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

DispatcherDurabilitySetupBest For
asyncio❌ Lost on crashZero — built-inDefault. Single-process apps, development, low-stakes tracking.
sync✅ Inline (blocking)Zero — built-inDebugging, unit testing, synchronous applications.
celery✅ Persisted in brokerRedis/RabbitMQ + workersProduction. Task retry, monitoring (Flower), multi-worker.
rq✅ Persisted in RedisRedis + workersSimpler alternative to Celery. Fewer features, easier setup.

Celery Dispatcher

Constructor Parameters

ParameterTypeDefaultDescription
celery_appCelery(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

ParameterTypeDefaultDescription
queuerq.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 CircuitBreakerOpenException without 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

ParameterTypeDefaultDescription
enabledboolTrueMaster switch for OpenTelemetry instrumentation. Set to False to completely disable tracing and metrics collection (zero overhead).
service_namestr"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_endpointstr"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 NameDescriptionAttributes
beliefstate.extract_beliefsMeasures fact extraction prompt latency and token usage.session_id, turn, beliefs_count, latency_ms
beliefstate.detect_contradictionTracks time taken by semantic filters and NLI logic check.session_id, candidates_checked, contradictions_found
beliefstate.resolve_beliefsTracks store write times and resolution conflicts.session_id, beliefs_added, beliefs_updated

Exported Metrics

Metric NameTypeDescription
beliefstate.beliefs_extractedCounterTotal number of facts parsed from conversation text.
beliefstate.contradictions_detectedCounterTotal number of logical contradictions detected.
beliefstate.beliefs_in_storeGaugeCurrent number of active beliefs in the store.
beliefstate.*_latency_msHistogramLatency 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

FieldTypeDescription
session_idstrThe session ID this event relates to.
operationstrThe pipeline stage: "extract_beliefs", "detect", "resolve", "store_write", etc.
turnintThe conversation turn number that triggered this event.
latency_msfloatExecution time in milliseconds for this operation.
detailstrHuman-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

FieldTypeDescription
session_idstrThe session ID whose data was deleted.
beliefs_deletedintNumber of belief records permanently removed from the store.
in_flight_tasks_drainedintNumber of background tracking tasks that were cancelled or waited for completion before deletion. Ensures no new beliefs are written after the delete.
deleted_atdatetimeUTC 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

  1. Drain in-flight tasks: The dispatcher cancels or awaits all active tracking tasks for the session.
  2. Delete from store: All beliefs for the session are permanently removed from the store (SQLite DELETE, Redis DEL).
  3. Clean up locks: Session-specific async locks, turn counters, and provider tracking entries are removed from memory.
  4. Generate receipt: A DeletionReceipt is returned with the count of deleted records and timestamp.

Best Practices

  • Store receipts: Log or persist DeletionReceipt objects as audit evidence for GDPR compliance reviews.
  • Redis TTL: For Redis stores, combine clear_session() with set_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.