Aira

Behavioral Drift Detection

Per-agent behavioral baselines, KL divergence scoring, and alerts when an agent's behavior shifts away from its expected pattern.

What it is

Drift detection answers a question that signed receipts can't: is this agent still doing what it's supposed to be doing?

A signed receipt proves "this action happened, signed by Aira." That is a per-action claim. Drift detection adds a per-agent claim: the agent's behavior over the last N hours matches the behavior we recorded as the baseline. When it doesn't, you get an alert with the exact action types that drifted, the KL divergence score, and the volume ratio.

The fingerprint is intentionally simple: action_type frequency distribution + average daily volume. This produces interpretable alerts ("the wire_transfer action went from 5% to 40% of total volume") and is independent of the agent's prompt or model. A drift alert is not a hallucination claim; it's a behavioral observation.


Three baseline types

Aira supports three kinds of baseline so you can avoid the "first N actions of a fresh agent are the baseline" attack surface.

TypeWhen to use
productionAfter the agent has been running for at least a few days. Computes the baseline from real action history in a window you specify.
syntheticAt first deployment, before any production data exists. Seed the baseline from a config dict ({"action_type": probability}) so the drift checker has a reference distribution from day 1.
pooledBootstrap a brand-new agent from a cohort of similar agents. Aggregates action history across multiple agents into a single reference.

Only one baseline is active per agent at any time. Past baselines are kept for audit but no longer compared against.


Compute a baseline

Production baseline (real history)

from datetime import datetime, timedelta, timezone
from aira import Aira

aira = Aira(api_key="aira_live_xxx")

now = datetime.now(timezone.utc)
window_start = now - timedelta(days=7)

baseline = aira._post(  # direct API call — SDK wrapper coming soon
    "/agents/payments-agent/drift/baseline",
    {
        "window_start": window_start.isoformat(),
        "window_end": now.isoformat(),
        "activate": True,
    },
)

Synthetic baseline (seeded)

baseline = aira._post(
    "/agents/payments-agent/drift/baseline/synthetic",
    {
        "expected_distribution": {
            "wire_transfer": 0.05,
            "email_sent": 0.40,
            "api_call": 0.30,
            "tool_call": 0.25,
        },
        "expected_actions_per_day": 200,
        "activate": True,
    },
)

Pooled baseline (cohort)

baseline = aira._post(
    "/agents/payments-agent-v2/drift/baseline/pooled",
    {
        "source_agent_ids": ["payments-agent", "payments-agent-canary"],
        "window_start": window_start.isoformat(),
        "window_end": now.isoformat(),
        "activate": True,
    },
)

Read the current drift status

GET /api/v1/agents/{slug}/drift?lookback_hours=24 returns the current divergence scored against the active baseline. Read-only — it does not persist an alert.

{
  "agent_id": "payments-agent",
  "has_baseline": true,
  "baseline": {
    "id": "...",
    "baseline_type": "production",
    "is_active": true,
    "action_type_dist": {
      "wire_transfer": 0.05,
      "email_sent": 0.40,
      "api_call": 0.55
    },
    "total_actions": 1400,
    "avg_actions_per_day": 200.0
  },
  "current_window": {
    "agent_id": "payments-agent",
    "action_type_dist": {
      "wire_transfer": 0.40,
      "email_sent": 0.20,
      "api_call": 0.40
    },
    "total_actions": 250,
    "avg_actions_per_day": 250.0
  },
  "kl_divergence": 0.4523,
  "volume_ratio": 1.25,
  "severity": "critical",
  "new_action_types": null,
  "is_drifting": true,
  "request_id": "..."
}

The dashboard polls this endpoint and renders a bar chart with the baseline as a faint outline behind the current window so the shift is visible at a glance.


Run a drift check (record an alert)

POST /api/v1/agents/{slug}/drift/check runs the same scoring but persists an alert if the divergence exceeds the threshold. Returns the new alert (or null if no drift).

curl -X POST https://api.airaproof.com/api/v1/agents/payments-agent/drift/check \
  -H "Authorization: Bearer aira_live_xxx"
{
  "id": "...",
  "agent_id": "payments-agent",
  "baseline_uuid": "...",
  "kl_divergence": 0.4523,
  "volume_ratio": 1.25,
  "severity": "critical",
  "new_action_types": ["data_export"],
  "detected_at": "2026-04-10T20:00:00Z",
  "acknowledged_at": null,
  "acknowledged_by": null
}

Run the check on a schedule (every 15 minutes is a reasonable default) and pipe the result into your alerting system. The check also fires the agent.drift_detected webhook event so external monitoring can hook in.


Severity bands

KL divergenceVolume ratioSeverityVerdict
< 0.300.2x .. 5xinfoStable — no alert raised
>= 0.30anywarningDistribution shift
any> 5x or < 0.2xwarningVolume spike or collapse
>= 0.90anycriticalSevere distribution shift
any> 10x or < 0.1xcriticalSevere volume anomaly

Thresholds are tunable per-call via the threshold parameter on check_drift. The defaults are calibrated against synthetic distributions to mark "this agent is doing something meaningfully different from yesterday" without firing on benign weekly patterns.


What KL divergence actually measures

Symmetric KL with Laplace smoothing. For two distributions P (baseline) and Q (current window):

KL_sym(P || Q) = 0.5 * (KL(P || Q) + KL(Q || P))

With Laplace smoothing (alpha = 1e-6) so that an action type appearing in one distribution but not the other contributes a finite penalty rather than infinity. This is the same primitive that drives the bar chart's "new action types" highlight: any key in the current window that's missing from the baseline is flagged in amber on the dashboard.

The math is pure-function and lives in app/services/drift_service.py on the backend if you want to inspect or replicate it.


Webhook payload

{
  "event": "agent.drift_detected",
  "agent_id": "payments-agent",
  "baseline_uuid": "...",
  "kl_divergence": 0.4523,
  "volume_ratio": 1.25,
  "severity": "critical",
  "new_action_types": ["data_export"],
  "detected_at": "2026-04-10T20:00:00Z"
}

Subscribe via Dashboard → Webhooks → Add endpoint → agent.drift_detected.

On this page