<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Banka Mani Bhargava</title>
    <description>The latest articles on DEV Community by Banka Mani Bhargava (@manibhargava4).</description>
    <link>https://dev.to/manibhargava4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047647%2Fd08e5438-2aeb-4f23-bfcd-51ca01d37d4e.jpg</url>
      <title>DEV Community: Banka Mani Bhargava</title>
      <link>https://dev.to/manibhargava4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manibhargava4"/>
    <language>en</language>
    <item>
      <title>Agent Black Box: Teaching an AI Agent to Explain Its Own Failures</title>
      <dc:creator>Banka Mani Bhargava</dc:creator>
      <pubDate>Sun, 26 Jul 2026 08:03:09 +0000</pubDate>
      <link>https://dev.to/manibhargava4/agent-black-box-teaching-an-ai-agent-to-explain-its-own-failures-2c38</link>
      <guid>https://dev.to/manibhargava4/agent-black-box-teaching-an-ai-agent-to-explain-its-own-failures-2c38</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built a record-guard-explain pipeline for AI agents on self-hosted SigNoz, and the three-layer metrics bug that taught me more about OpenTelemetry than the docs did.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most agent observability stops at "send some spans to a dashboard." Agent Black Box adds live guardrails that kill runaway runs and an automated postmortem agent that explains failures after the fact, using SigNoz's own MCP server.&lt;/li&gt;
&lt;li&gt;Getting agent metrics to show up correctly took fixing three separate, independent bugs: a missing flush, an instance-ID cardinality explosion, and a temporality mismatch, each one looking identical from the dashboard ("No Data" or a wrong zero) but with a genuinely different root cause.&lt;/li&gt;
&lt;li&gt;A SigNoz alert form silently saved two thresholds as &lt;code&gt;0&lt;/code&gt; and inverted a comparison operator, no error shown. Lesson: verify anything configured through a UI by reading it back through the API, not by trusting the screen.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The hook
&lt;/h2&gt;

&lt;p&gt;My test agent called the same search tool 47 times in a row and burned real API cost before anything told me. Nothing crashed. No log said anything was wrong. It just kept going, the way a car with a stuck accelerator keeps going, right up until it doesn't.&lt;/p&gt;

&lt;p&gt;That's the actual failure mode of AI agents in 2026, and it's the one almost nobody instruments for. Most "agent observability" means shipping a few spans to a dashboard and hoping you're watching it at the moment it breaks. You're not. You're asleep, or in a meeting, or building something else, and the agent is quietly turning your token budget into nothing.&lt;/p&gt;

&lt;p&gt;I built Agent Black Box to fix that. It &lt;strong&gt;records&lt;/strong&gt; every agent run as real OpenTelemetry spans, &lt;strong&gt;guards&lt;/strong&gt; the run live so a runaway loop or cost spike gets killed before it does more damage, and, the part that took the longest and mattered the most, &lt;strong&gt;explains&lt;/strong&gt; what happened afterward, by having a second agent query the observability backend itself and write a root-cause report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, precisely
&lt;/h2&gt;

&lt;p&gt;Three things are true about AI agents that make them hard to watch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They fail silently.&lt;/strong&gt; A stuck loop doesn't throw an exception. A cost spiral doesn't page anyone. The agent just keeps doing the wrong thing, successfully, from its own point of view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure signal and the root cause are different things.&lt;/strong&gt; A guardrail catching a loop tells you &lt;em&gt;a loop happened&lt;/em&gt;. It doesn't tell you &lt;em&gt;why the agent decided to repeat itself&lt;/em&gt;, that's upstream, in the reasoning, and you have to go dig for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody wants to dig.&lt;/strong&gt; After an incident, the actual investigative work, pulling the trace, cross-referencing logs, checking token counts, is exactly the tedious work a second AI agent is good at, if you give it the tools to do it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the design became three pieces, in one sentence: &lt;strong&gt;record everything properly, kill bad runs live, and have an agent read the wreckage back and explain it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────────────────┐
  your agent  ──►   │  agent_blackbox.trace()      │
  (any LLM /        │   • gen_ai.* semconv spans   │   OTLP
   framework)       │   • token + cost attributes  │  ──────►  SigNoz (Foundry, :8080)
                    │   • guardrail span processor │  :4318         │
                    └──────────────────────────────┘                │
                                    │                               │
                          RunawayAgentError                         │
                          (loop / cost / error cascade)             │
                                                                    │
  $ blackbox postmortem &amp;lt;trace_id&amp;gt;                                  │
        └─► LLM agent ──MCP──► signoz_get_trace_details ────────────┘
             (localhost:8000)    signoz_search_logs
                                 signoz_query_metrics
             └─► Root-cause report (markdown, printed + saved)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozd4a3uwqeq8r75eu883.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozd4a3uwqeq8r75eu883.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
Everything runs against &lt;strong&gt;self-hosted SigNoz&lt;/strong&gt;, deployed with &lt;a href="https://github.com/SigNoz/foundry" rel="noopener noreferrer"&gt;Foundry&lt;/a&gt;. One YAML file (&lt;code&gt;casting.yaml&lt;/code&gt;) and one command (&lt;code&gt;foundryctl cast&lt;/code&gt;) brings up SigNoz &lt;em&gt;and&lt;/em&gt; its MCP server together. That MCP server is the piece that makes the "explain" step possible: it's a real, first-party interface an LLM can call to query traces, logs, and metrics like any other tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  The recorder: making agents speak OpenTelemetry
&lt;/h2&gt;

&lt;p&gt;The public API is deliberately tiny: two lines to wrap an agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_blackbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traced_llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traced_tool&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;traced_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;traced_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;search_fn&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this emits proper &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic-convention&lt;/a&gt; spans, &lt;code&gt;invoke_agent&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt;, &lt;code&gt;execute_tool&lt;/code&gt;, plus a small custom &lt;code&gt;agent.*&lt;/code&gt; namespace for what the spec doesn't cover yet: loop signatures, run cost, guardrail trips. Getting the semconv attributes right mattered more than I expected going in. A trace that &lt;em&gt;looks&lt;/em&gt; like a GenAI trace but doesn't use the standard attribute names is a trace SigNoz's Query Builder can't group or aggregate on, and the whole dashboard depends on that grouping working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails: making the kill itself observable
&lt;/h2&gt;

&lt;p&gt;Four detectors run as a &lt;code&gt;SpanProcessor&lt;/code&gt; watching the span stream live: loop detection (same tool and same arguments, repeated), a cost ceiling, an error cascade, and a depth limit for nested agents. Each one raises a &lt;code&gt;RunawayAgentError&lt;/code&gt;, but the interesting design decision was making the &lt;em&gt;kill itself&lt;/em&gt; show up in SigNoz, not just in a Python traceback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail.trip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AGENT_GUARDRAIL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guardrail_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AGENT_GUARDRAIL_REASON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RunawayAgentError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guardrail_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A guardrail that fires silently in application logs is a guardrail nobody trusts. This one leaves a &lt;code&gt;guardrail.trip&lt;/code&gt; span and a metric increment, so the kill is exactly as visible as the failure it prevented.&lt;/p&gt;

&lt;h2&gt;
  
  
  The differentiator: a postmortem agent that reads its own trace
&lt;/h2&gt;

&lt;p&gt;This is the part I'm actually proud of. &lt;code&gt;blackbox postmortem &amp;lt;trace_id&amp;gt;&lt;/code&gt; connects to the SigNoz MCP server as a genuine MCP client, gives Claude the real tool list (&lt;code&gt;signoz_get_trace_details&lt;/code&gt;, &lt;code&gt;signoz_search_logs&lt;/code&gt;, &lt;code&gt;signoz_query_metrics&lt;/code&gt;), and lets it investigate: not summarize a template, actually look at the evidence and reason about it.&lt;/p&gt;

&lt;p&gt;Here's an unedited excerpt from a real report, generated against a trace where the loop-detection guardrail fired:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;&lt;br&gt;
The guardrail (&lt;code&gt;guardrail.trip&lt;/code&gt;) is a symptom detector, not the cause, it worked as designed, catching a runaway pattern and aborting the run before it could burn more resources. The actual root cause is upstream in the agent's decision loop: the agent issued the exact same &lt;code&gt;web_search&lt;/code&gt; tool call three times in a row... Notably, the trace contains &lt;strong&gt;no LLM/completion span&lt;/strong&gt; between the tool calls, so the instrumentation does not show what reasoning/prompt state produced the repeated call. This is an observability gap that prevents confirming whether the repetition originated from the LLM output itself or from an orchestration bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3917jcuqlg2fyshaqljf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3917jcuqlg2fyshaqljf.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
Nobody told it to distinguish "symptom" from "cause." Nobody told it to notice the missing LLM span and flag it as its own limitation. That's what evidence-grounded reasoning over real telemetry looks like when it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like when it's all wired together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbpjqh6c0xvo2rm2k8rv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbpjqh6c0xvo2rm2k8rv5.png" alt=" " width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seven panels, all built with SigNoz's Query Builder: runs by outcome, latency percentiles, token burn and cost by model, tool success rate, slowest tool calls, and guardrail trips. Four alert rules watch cost spikes, tool error rates, guardrail trips, and, the one I like most, an absent-data alert that catches the agent going &lt;em&gt;silent&lt;/em&gt; instead of erroring loudly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbgr9drgdng11zabvy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbgr9drgdng11zabvy8.png" alt=" " width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong (the honest part)
&lt;/h2&gt;

&lt;p&gt;The recorder, the guardrails, the postmortem agent, all of that came together in a day. &lt;strong&gt;The dashboard metrics took three separate bugs to get right&lt;/strong&gt;, and each one taught me something that isn't obvious from the OpenTelemetry docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: metrics never left the process.&lt;/strong&gt; Every dashboard panel built on traces worked immediately. Every panel built on metrics said "No Data." The cause: my demo scripts are short-lived CLI processes that finish in a couple of seconds, well under &lt;code&gt;PeriodicExportingMetricReader&lt;/code&gt;'s default 15-second export interval, so the reader's own timer never got a chance to fire before the process exited. &lt;code&gt;shutdown()&lt;/code&gt; alone doesn't force a final flush; you need an explicit &lt;code&gt;force_flush()&lt;/code&gt; first. Traces looked fine because &lt;code&gt;BatchSpanProcessor.shutdown()&lt;/code&gt; &lt;em&gt;does&lt;/em&gt; flush reliably. The same assumption just doesn't hold for the metrics reader.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: metrics arrived, then fragmented into uselessness.&lt;/strong&gt; After the flush fix, "No Data" became sparse, near-empty results. The SigNoz Metrics Explorer is what actually cracked this one: one metric had 16 samples spread across 14 separate time series, nearly one series per sample. The cause was OpenTelemetry auto-generating a random &lt;code&gt;service.instance.id&lt;/code&gt; per process, since I never set one explicitly. Every CLI invocation is a new process, so every run got its own random instance ID and its own brand-new, single-point series. &lt;code&gt;Increase&lt;/code&gt;/&lt;code&gt;Rate&lt;/code&gt; need at least two points &lt;em&gt;within the same series&lt;/em&gt; to compute a delta, and a series with one point has nothing to compute against. Fix: pin &lt;code&gt;service.instance.id&lt;/code&gt; to a constant string, so repeated runs of the same script accumulate into one continuous series instead of scattering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3: consolidated, but still computing the wrong number.&lt;/strong&gt; Even after fixing cardinality, the "Guardrail trips" panel stubbornly showed &lt;code&gt;0&lt;/code&gt;, despite the guardrail having tripped at least five times across my test runs. This is the one that actually taught me something about OpenTelemetry's default assumptions: &lt;strong&gt;cumulative temporality assumes one long-running process.&lt;/strong&gt; Each of my short-lived processes started its own counter at zero and typically reported exactly one value, "1 trip", before exiting. Across three separate processes that each tripped once, the raw exported values looked like &lt;code&gt;[1, 1, 1]&lt;/code&gt;. A naive last-minus-first delta across those points is &lt;code&gt;1 - 1 = 0&lt;/code&gt;, which is &lt;em&gt;correct&lt;/em&gt; math for a single continuously-running counter and &lt;em&gt;wrong&lt;/em&gt; for three independent ones. The fix wasn't in any query, it was one environment variable: &lt;code&gt;OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta&lt;/code&gt;, so each export reports "+1 this interval" directly instead of a cumulative-since-start value that doesn't compose across processes.&lt;/p&gt;

&lt;p&gt;Three bugs, each one masquerading as "still no data," each with a genuinely different root cause. The lesson that generalizes: &lt;strong&gt;OpenTelemetry's defaults are tuned for one long-running server, not a fleet of short CLI invocations.&lt;/strong&gt; If that's your workload, expect to fight the flush timing, the resource cardinality, and the temporality model, in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smaller lessons
&lt;/h2&gt;

&lt;p&gt;A few more from the build log, because they're the kind of thing you only learn by hitting them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz's IAM has a two-step trap.&lt;/strong&gt; Creating an API key under a service account doesn't grant that account read access to observability data. You also need to assign it a role (&lt;code&gt;signoz-admin&lt;/code&gt; or &lt;code&gt;signoz-viewer&lt;/code&gt;) under Settings -&amp;gt; Roles, which is a separate page the API-key creation flow never mentions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A UI form can silently save the wrong value.&lt;/strong&gt; Two of my four alert rules saved with &lt;code&gt;target: 0&lt;/code&gt; instead of the real threshold, and a third saved with the comparison operator inverted, no error, no warning, the UI just reported success. I only caught it by checking the saved rule against the API (&lt;code&gt;GET /api/v2/rules&lt;/code&gt;) instead of trusting what the form showed. That's now a standing habit: verify anything you configure through a UI by reading it back, not by trusting the screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two guardrails can both be "correct" about the same symptom.&lt;/strong&gt; My loop detector and error-cascade detector both watch the same tool-call stream, and when a tool is both looping &lt;em&gt;and&lt;/em&gt; failing, whichever check runs first in the code wins the race. That's an implementation detail, not a judgment call, and it's worth being honest about instead of pretending the system is smarter than it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility is worth testing for real, not assuming.&lt;/strong&gt; I tore down the whole deployment, cloned the repo fresh, and ran &lt;code&gt;foundryctl cast -f casting.yaml&lt;/code&gt; against nothing but the two committed files. It came back up clean: UI at &lt;code&gt;200&lt;/code&gt;, MCP correctly demanding auth at &lt;code&gt;401&lt;/code&gt;. That's a much stronger claim than "it should be reproducible."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not just use SigNoz Cloud instead of self-hosting?&lt;/strong&gt;&lt;br&gt;
Self-hosting via Foundry means the whole deployment, SigNoz plus its MCP server, is defined in one committed YAML file. That makes the setup reproducible from a clean clone with a single command, which matters if you want anyone else to verify it actually works rather than take your word for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between this and just asking an LLM to look at a SigNoz dashboard?&lt;/strong&gt;&lt;br&gt;
The guardrails run live, during the agent's execution, not after. A dashboard only tells you what already happened; the guardrail span processor can kill a run mid-flight, before it burns more cost or keeps looping. The postmortem step is separate and happens after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the postmortem agent ever get it wrong?&lt;/strong&gt;&lt;br&gt;
It's bounded by what the trace, logs, and metrics actually contain. In one real run, it correctly flagged that it couldn't confirm the root cause because the trace had no LLM span to inspect, rather than guessing. That's the behavior I wanted: reasoning grounded in the available evidence, with the gaps stated as gaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this work with agent frameworks other than a raw Anthropic client?&lt;/strong&gt;&lt;br&gt;
The recorder wraps any callable, &lt;code&gt;traced_llm&lt;/code&gt; and &lt;code&gt;traced_tool&lt;/code&gt; are decorators around whatever function makes the actual call, so it isn't tied to one SDK. Anything that returns token usage in its response object can be adapted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The technically hardest part of this project wasn't the AI agent. It was making sure short-lived processes report metrics correctly, a problem OpenTelemetry's defaults quietly assume you don't have. The most interesting part was watching a postmortem agent correctly reason that a guardrail is a symptom detector, not a root cause, without being told to think that way.&lt;/p&gt;

&lt;p&gt;Record. Guard. Explain. Full-circle observability tells you what happened. Agent Black Box stops it happening, and explains it afterward.&lt;/p&gt;

&lt;p&gt;Code, dashboard export, alert rules, and the full build log are on GitHub: &lt;strong&gt;&lt;a href="https://github.com/manibhargava4/agent-blackbox" rel="noopener noreferrer"&gt;agent-blackbox&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>ai</category>
      <category>observability</category>
      <category>python</category>
    </item>
  </channel>
</rss>
