fix: Agent Behavior signals (loop/failure taxonomy) + Agent Graph fixes (#13279)

## What

Adds **Agent Behavior signals** — a PB-safe rollup that surfaces two
behavioural questions the platform couldn't answer before: *is an agent
stuck in a loop?* and *why is it failing?* Plus a batch of Agent Graph /
AI Observability UX and correctness fixes found while building it.

Companion changes land in **o2-enterprise**
(`feat/agent-signals-enterprise-split`): the SQL builders, mappers,
config-driven failure taxonomy, the service-graph agent-classification
fix, and the RBAC route entry.

## Backend (agent-signals)
- Bounded-key rollup co-located in the service-graph job
(alert-manager-only, reuses its watermark + self-ingest). Groups only on
agent/tool/class — never trace_id — so it stays PB-safe.
- Config-driven **failure taxonomy** (rules as data, evolvable without
code) + config-driven error-detail columns; ancestor-agent resolution so
error spans attribute to the owning agent.
- Read API `GET /{org}/traces/agent_signals` (dual `#[cfg]`, reads the
derived `_agent_signals` stream) + RBAC route entry (enterprise
`route_permissions`) mapping it to `traces` read.
- OSS/enterprise split respected: pure SQL/mapping in enterprise, thin
gated I/O in OSS.
- Fix: `traces/latest` only narrows to a UUIDv7 trace_id's embedded time
when that time is inside the caller's window (fixes "trace not found"
for time-shifted/re-ingested traces).

## Frontend (AI Observability)
- New **Agent Behavior** page under Monitor: looping-agents +
failure-taxonomy tables, each row opening an actionable detail drawer
(real grouped error messages / worst-repeat traces), click-through to
Trace Details in a new tab.
- Agent-node **Behavior tab** on the Agent Graph node drawer (reuses the
same signals).
- Nav rename: LLM Insights → **Insights**, LLM Sessions → **Sessions**.
- **Agent** is the default scope across the AI pages, with the
Agent|Stream toggle placed consistently (left scope bar); explicit
URL/localStorage choices still win.
- Removed the duplicate in-table refresh on Sessions.

## Agent Graph fixes
- **Agent classification**: agents that also call tools now correctly
classify as `service_type: "agent"` (they glow), fixing a latent
precedence bug where the tool/model client-role overrode the agent role
and `is_real_service` forced agents to plain services. Rust
`classify_entity` brought in line with the (already-correct) TS twin.
- **Drag-to-pan restored**: the shared ChartRenderer armed
`dataZoomSelect` cursor unconditionally, hijacking drag away from roam
pan. Disabled for this graph (no dataZoom).
- **Nodes scale on zoom** (`nodeScaleRatio: 1`) so zooming enlarges
nodes/labels, not just spacing.

## Testing
- Enterprise unit tests: agent-signals SQL/mappers/config,
classification precedence, RBAC route resolution — all pass.
- FE: type-check + lint clean.
- Verified end-to-end on real re-ingested introspection data: taxonomy
unclassified rate ~100% → ~1–3%, agents resolved, both agents
classify/glow, trace-open works.

## Notes
- `Cargo.toml`/`Cargo.lock` and unrelated Slack-bot files were
intentionally excluded from the commits (local build-config drift).

---------

Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
This commit is contained in:
Ashish Kolhe
2026-07-21 10:45:05 +00:00
committed by GitHub
co-authored by ktx-vaidehi ktx-vaidehi
parent 36518e49c1
commit 9ede45bbf7
34 changed files with 2707 additions and 113 deletions
+15 -1
View File
@@ -36,6 +36,8 @@ use tracing::{Instrument, Span};
#[cfg(feature = "cloud")]
use crate::service::ingestion::check_ingestion_allowed;
// Re-export agent-signals read API handler
pub use crate::service::traces::agent_signals::get_agent_signals;
// Re-export service graph API handlers
pub use crate::service::traces::service_graph::{self, get_current_topology, get_edge_history};
use crate::{
@@ -325,7 +327,19 @@ pub async fn get_latest_traces(
0
};
if start_time_from_trace_id > 0 {
// A UUID v7 trace_id encodes its creation time, which lets us narrow the
// scan to a tight [t-60s, t+1h] window instead of the caller's broad range.
// BUT only when that encoded time actually falls inside the window the
// caller asked for. If it doesn't — e.g. spans re-ingested with a
// `_timestamp` shifted away from the trace_id's embedded time — the encoded
// time points at an empty window and the lookup would spuriously return
// "trace not found". In that case we trust the caller's window, which is
// where the data actually is. (On real data the two always agree, so this
// is a safety net, not a behavior change for normal traffic.)
if start_time_from_trace_id > 0
&& start_time_from_trace_id >= start_time
&& start_time_from_trace_id <= end_time
{
start_time = start_time_from_trace_id - 60 * 1_000_000; //60 seconds earlier
end_time = std::cmp::min(now_micros(), start_time_from_trace_id + 3600 * 1_000_000); //1 hour later
}