{% extends "base.html" %} {% block title %}Drift Monitor — AgentPulse{% endblock %} {% block content %}

Drift Monitor

See where your multi-agent system is changing, what looks risky, and which agents or handoffs need attention.
{% macro _qs(days, tt, ver) -%} ?days={{ days }}{% if tt %}&task_type={{ tt }}{% endif %}{% if ver %}&version={{ ver }}{% endif %} {%- endmacro %}
Window: {% for d in [7, 30, 90, 365] %} {{ d if d < 365 else 'all' }}{{ 'd' if d < 365 else '' }} {% endfor %}
Task type: all {% for tt in task_types %} {{ tt }} {% endfor %}
Version: all {% if versions_available %} {% for v in versions_available %} v{{ v }} {% endfor %} {% else %} (no versions in window) {% endif %}
{% if trends.insufficient %}
Not enough runs in this window — found {{ trends.runs_count }}, need at least {{ trends.min_required }} to compute trends.
Widen the time window or capture more runs.
{% else %} {% set hs = trends.health_summary %}
{{ hs.runs_analyzed }}
Runs analyzed
{{ trends.n_early }} earlier · {{ trends.n_recent }} recent
{{ hs.drift_total }}
Drift suspects
{{ hs.drift_handoffs }} handoffs · {{ hs.drift_agents }} agent{{ '' if hs.drift_agents == 1 else 's' }}
{{ "%+.0f"|format(hs.cost_pct) }}%
Cost per run
Recent vs earlier
{{ "%+.0f"|format(hs.wall_pct) }}%
Wall-clock per run
Recent vs earlier
{% if trends.versions and trends.versions|length %}
Versions
Manual snapshots of your settings. Compare iterations side by side.
{% for v in trends.versions %} {% endfor %}
VersionRunsSuccessAvg costAvg tokensChanged from previous
{{ v.label }} {{ v.runs }} {{ "%.0f"|format(v.success_rate) }}% ${{ "%.4f"|format(v.avg_cost) }} {{ "{:,}".format(v.avg_tokens) }} {% if v.diff %} {% for d in v.diff %}
{{ d.dimension }}{% if d.scope != 'workflow' %} · {{ d.scope }}{% endif %}: {{ d.old }}{{ d.new }}
{% endfor %} {% elif not loop.first %}no config change {% else %}baseline{% endif %}
{% endif %} {% if trends.drift_suspects %}
Top handoff drift suspects
Handoffs (agent → agent) whose behaviour shifted most when comparing the latest runs to earlier ones in this window.
{% for s in trends.drift_suspects %}
{{ s.from }} {{ s.to }}
Risk: {{ s.risk|capitalize }} {% if s.risk_reason %}
{{ s.risk_reason }}
{% endif %}
Drift type
{{ s.rule_title }}{% if s.also_high_volume %} + previously high volume{% endif %}
{% if s.also_titles %}
also detected: {{ s.also_titles|join(', ') }}
{% endif %}
{% if s.insight %}
Insight
{{ s.insight }}
{% endif %} {% if s.replacement %}
Routing impact
{{ s.replacement }}
{% endif %} {% if s.evidence %}
Evidence
{% for row in s.evidence %} {% if row.pct is none %} {% else %} {% set cls = 'flat' if row.pct == 0 else ('up' if row.pct > 0 else 'down') %} {% endif %} {% endfor %}
{{ row.label }} {{ row.recent }}{{ row.note or '—' }}{{ "%+.0f"|format(row.pct) }}%
{% endif %} {% if s.downstream %}
Downstream impact
{% for row in s.downstream %} {% if row.pct is none %} {% else %} {% set cls = 'flat' if row.pct == 0 else ('up' if row.pct > 0 else 'down') %} {% endif %} {% endfor %}
{{ row.label }} {{ row.recent }}{{ row.note or '—' }}{{ "%+.0f"|format(row.pct) }}%
{% endif %} {% if s.observed_value %}
{{ s.observed_label }}
{{ s.observed_value }}
{% elif s.attribution %}
{{ s.observed_label or 'Appeared in' }}
{{ s.attribution }}
{% endif %}
{% endfor %}
{% endif %} {# "Other" only makes sense when a flagged-suspects section precedes this one. #}
{{ 'Other common handoffs' if trends.drift_suspects else 'Common handoffs' }}
{% if trends.drift_suspects %} The most frequent routes in this window, by volume, with the same metrics as the suspects above. A badge appears if a route also drifted but wasn't a top suspect. {% else %} The most frequent routes in this window, by volume. A badge appears if a route drifted. {% endif %}
{% if trends.top_handoffs.pairs %} {% for p in trends.top_handoffs.pairs %}
{{ p.from }} {{ p.to }} {% if p.minor_drift %} minor drift {% endif %}
{{ p.success_rate }}%
Volume
Fires {{ p.count }} times ({{ p.pct }}% of all handoffs)
{% if p.evidence %}
Evidence
{% for row in p.evidence %} {% if row.pct is none %} {% else %} {% set cls = 'flat' if row.pct == 0 else ('up' if row.pct > 0 else 'down') %} {% endif %} {% endfor %}
{{ row.label }} {{ row.recent }}{{ row.note or '—' }}{{ "%+.0f"|format(row.pct) }}%
{% endif %} {% if p.downstream %}
Downstream impact
{% for row in p.downstream %} {% if row.pct is none %} {% else %} {% set cls = 'flat' if row.pct == 0 else ('up' if row.pct > 0 else 'down') %} {% endif %} {% endfor %}
{{ row.label }} {{ row.recent }}{{ row.note or '—' }}{{ "%+.0f"|format(row.pct) }}%
{% endif %}
{% endfor %} {% else %}
No handoffs captured in this window — agents may run independently without explicit transitions.
{% endif %}
{% if trends.agent_drift and (trends.agent_drift.drift or trends.agent_drift.info) %}
Agent drift summary
Per-agent behaviour change, recent runs vs earlier runs. Values are colored by risk, not by direction.
{% for a in trends.agent_drift.drift %}
{{ a.agent }}
seen in {{ a.runs_seen }} run(s)
{% set status_pill = {'critical':'drift','high':'drift','medium':'potential','watch':'watch','stable':'ok'} %} {% set status_text = {'critical':'Critical','high':'High risk','medium':'Medium risk','watch':'Watch','stable':'Stable'} %}
{{ status_text[a.status] }} {% if a.confidence %}
{{ a.confidence }}
{% endif %}
Insight
{{ a.insight }}
Evidence
{% for row in a.evidence %} {% endfor %}
{{ row.label }} {{ row.recent }} {{ "%+.0f"|format(row.pct) }}%
Impact
{% for row in a.impact %} {% endfor %}
{{ row.label }} {{ row.recent }} {{ "%+.0f"|format(row.pct) }}%
{% endfor %} {# Lightweight info cards for agents that only appear in one half #} {% for a in trends.agent_drift.info %}
{{ a.agent }}
seen in {{ a.runs_seen }} run(s)
{{ 'Newly observed' if a.info_type=='new' else 'Earlier only' }}
{{ a.note }}
{{ 'Recent snapshot' if a.info_type=='new' else 'Earlier snapshot' }}
{% for row in a.snapshot %} {% endfor %}
{{ row.label }} {{ row.value }}
{% endfor %}
{% endif %} {% if trends.path_summary %} {% set ps = trends.path_summary %}
Path summary
How the overall agent paths through the system are changing, recent runs vs earlier runs.
Avg path length
{{ ps.avg_path_length.value }} agents
{{ "%+.0f"|format(ps.avg_path_length.pct) }}%
Unique paths
{{ ps.unique_paths.value }}
{{ "%+.0f"|format(ps.unique_paths.pct) }}%
Re-invocation loops
{{ ps.reinvocation_loops.value }} runs
{{ "%+.0f"|format(ps.reinvocation_loops.pct) }}%
Most changed route
{% if ps.most_changed_route %} {% set mc = ps.most_changed_route %}
{{ mc.from }} {{ mc.to }}
{{ mc.agents }} agents · {{ mc.handoffs }} handoffs{% if mc.cost_pct is not none %} · {{ "%+.0f"|format(mc.cost_pct) }}% cost{% endif %}
{% else %}
{% endif %}
{% endif %} {% if trends.parallel_health %} {% set ph = trends.parallel_health %}
Parallel group health
Fan-out behaviour of the primary parallel group, recent runs vs earlier runs.
{% if ph.available %}
{# Bottleneck branch — slowest branch gating the join. Up = worse. #}
Bottleneck branch
{{ "%.1f"|format(ph.bottleneck_ms.early/1000) }}s {{ "%.1f"|format(ph.bottleneck_ms.recent/1000) }}s
{{ "%+.0f"|format(ph.bottleneck_ms.pct) }}%
{# Join wait — how long the fastest branch idled. Up = worse. #}
Join wait
{{ "%.1f"|format(ph.join_wait_ms.early/1000) }}s {{ "%.1f"|format(ph.join_wait_ms.recent/1000) }}s
{{ "%+.0f"|format(ph.join_wait_ms.pct) }}%
{# Efficiency — balance of the fan-out. Down = worse. #}
Efficiency
{{ "%.0f"|format(ph.efficiency.early*100) }}% {{ "%.0f"|format(ph.efficiency.recent*100) }}%
{{ "%+.0f"|format(ph.efficiency.pct) }}%
{% else %}
Not enough runs with a parallel group yet ({{ ph.n_early }} early · {{ ph.n_recent }} recent) — need at least one in each half.
{% endif %} {% endif %} {% if trends.related_changes and trends.related_changes.has_config %} {% set rc = trends.related_changes %}
Potentially related changes
Config changes in the {{ rc.lookback_runs }} runs or {{ rc.lookback_hours }}h before the recent window — temporal correlation only, not a confirmed cause.
{% if rc.changed %}
Changed shortly before the drift:
{% else %}
No config changes detected in the lookback window.
{% endif %} {% if rc.unchanged %}
Stayed the same: {% for d in rc.unchanged %}{{ d }} {% endfor %}
{% endif %}
These are clues to investigate — the tool does not assert any of them caused the drift.
{% endif %}
Cost per run trend
One point per run. Use with the cards above to explain cost changes.
Wall clock per run trend
End-to-end run time. Pairs with latency drift on the cards above.
{% endif %} {% endblock %} {% block scripts %} {% endblock %}