<?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: Abhishek Gupta</title>
    <description>The latest articles on DEV Community by Abhishek Gupta (@abhishek_gupta_b7379457d6).</description>
    <link>https://dev.to/abhishek_gupta_b7379457d6</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%2F4016032%2F50f7f3ae-9bed-47ce-ba9d-078ad2a3ca0e.png</url>
      <title>DEV Community: Abhishek Gupta</title>
      <link>https://dev.to/abhishek_gupta_b7379457d6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhishek_gupta_b7379457d6"/>
    <language>en</language>
    <item>
      <title>I Built a Trace Funnel Over My AI Agent. The Conversion Rate Came Back NaN.</title>
      <dc:creator>Abhishek Gupta</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:26:23 +0000</pubDate>
      <link>https://dev.to/abhishek_gupta_b7379457d6/i-built-a-trace-funnel-over-my-ai-agent-the-conversion-rate-came-back-nan-5f7e</link>
      <guid>https://dev.to/abhishek_gupta_b7379457d6/i-built-a-trace-funnel-over-my-ai-agent-the-conversion-rate-came-back-nan-5f7e</guid>
      <description>&lt;p&gt;I asked SigNoz a simple question: of the runs where my agent planned an answer and called its search tool, how many reached the LLM step? The funnel said zero, for a model that had demonstrably run in ten traces. Chasing that zero led somewhere better than the answer I wanted: an analytics endpoint that returns HTTP 500 where "0%" belongs, and a one-line guard in the SQL that explains exactly why.&lt;/p&gt;

&lt;p&gt;This is a warm-up post for the Agents of SigNoz hackathon. I self-hosted SigNoz v0.132.2 with Foundry (&lt;a href="https://signoz.io/docs/install/docker/" rel="noopener noreferrer"&gt;the docs walk through it&lt;/a&gt;), pointed a small AI agent at it, and spent a few days on a feature I hadn't seen anywhere else. My first warm-up post covered trace-to-log correlation; this one is a different corner of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why funnels, and why over agent traces
&lt;/h2&gt;

&lt;p&gt;Conversion funnels are an old product-analytics idea: land, add to cart, check out, measure the drop-off at each step. SigNoz took that shape and pointed it at traces. You define an ordered list of span-name steps, and it tells you what fraction of traces made it through each one. I went hunting for this in the LLM-observability tools I already knew, and came up empty. Langfuse, LangSmith, Phoenix, Braintrust: plenty of tracing, but I didn't find a conversion funnel over trace steps in any of them.&lt;/p&gt;

&lt;p&gt;For an agent, that shape is exactly right. An agent run is a small pipeline. Plan, call a tool, call the model, answer. If some fraction of runs never reach the model step, or the tool step silently drops out after a deploy, a funnel is the cleanest way to see it. So I opened Traces, found the Funnels section, and started building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data I was working with
&lt;/h2&gt;

&lt;p&gt;A single session of agent runs left 22 traces under one service, &lt;code&gt;warmup-agent&lt;/code&gt;. Small n, but as it turns out, more than enough to hit the edge. Every trace has an &lt;code&gt;agent answer&lt;/code&gt; span and a &lt;code&gt;tool search_tool&lt;/code&gt; span. Then there's the LLM step, and this is where it gets interesting. I switched models a few times during that session, and my spans are named following the OpenTelemetry GenAI semantic conventions (still marked experimental), which &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md" rel="noopener noreferrer"&gt;specify the span name as &lt;code&gt;{operation} {model}&lt;/code&gt;&lt;/a&gt;. One logical operation, but the name changes with the model.&lt;/p&gt;

&lt;p&gt;I confirmed the shape straight from ClickHouse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec signoz-telemetrystore-clickhouse-0-0 clickhouse-client -q "SELECT name, count() AS spans, count(DISTINCT trace_id) AS traces FROM signoz_traces.distributed_signoz_index_v3 WHERE name LIKE 'chat %' GROUP BY name ORDER BY spans DESC FORMAT PrettyCompactMonoBlock"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ┌─name───────────────────────┬─spans─┬─traces─┐
1. │ chat gpt-4o-mini           │    20 │     10 │
2. │ chat gemini-3.1-flash-lite │    16 │      8 │
3. │ chat gpt-oss-120b          │     8 │      4 │
   └────────────────────────────┴───────┴────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 44 chat spans across 22 traces, split three ways by model. The &lt;code&gt;agent answer&lt;/code&gt; and &lt;code&gt;tool search_tool&lt;/code&gt; spans match all 22 traces each; the LLM step matches at most 10 of them under any single name. This is correct instrumentation. The convention says the model belongs in the span name, my exporter obeys, and the result is that "the LLM call" has no single name to funnel on.&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%2Ftdkflcgq5xwol1ivyyu4.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%2Ftdkflcgq5xwol1ivyyu4.png" alt="Agent reasoning loop: one logical LLM step fragments into three span names" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the naming, straight from my 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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.operation.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the funnel
&lt;/h2&gt;

&lt;p&gt;My first funnel had three steps in the order I think of my agent: &lt;code&gt;agent answer&lt;/code&gt;, then &lt;code&gt;tool search_tool&lt;/code&gt;, then &lt;code&gt;chat gpt-4o-mini&lt;/code&gt;.&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%2Focpjviatoojkreqhfb25.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%2Focpjviatoojkreqhfb25.png" alt="The agent-pipeline funnel configured in SigNoz: three steps on the warmup-agent service" width="535" height="765"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Steps one and two matched 22 out of 22. Step three matched zero.&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%2Fgqsfh0p7nrsxd11jhivf.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%2Fgqsfh0p7nrsxd11jhivf.png" alt="Funnel results: 22 and 22 span bars, then 0 with a 100% drop, while the overview panel shows No data" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zero. For a model that demonstrably ran in ten traces. At first I assumed I'd broken my own instrumentation. That's the honest reflex: when a tool shows zeros on your data, you suspect your data. I went back to my exporter config, re-checked the span names against ClickHouse, even built a one-step funnel containing only &lt;code&gt;chat gpt-4o-mini&lt;/code&gt;. That one matched 10 traces. So the spans exist, the names are right, and the funnel still says zero when the step sits third.&lt;/p&gt;

&lt;p&gt;The explanation took me longer than I'd like to admit: funnels enforce strict temporal ordering within each trace, taking the &lt;em&gt;first&lt;/em&gt; matching span per step, and my agent calls the LLM to &lt;em&gt;plan&lt;/em&gt; before it calls the tool. The first &lt;code&gt;chat&lt;/code&gt; span in every trace fires before &lt;code&gt;tool search_tool&lt;/code&gt;, so no trace satisfies "chat after tool," and step three legitimately converts to zero. Fair enough, that's what a funnel means. But notice what the number can't tell you: a step reading 0 looks identical whether the model never ran or ran a thousand times in a different order.&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%2Fqyie4h2ky8ah6v3pa9uw.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%2Fqyie4h2ky8ah6v3pa9uw.png" alt="One trace in time order: the funnel timestamps chat at the early plan call, so chat-after-tool matches nothing" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reordering the steps to &lt;code&gt;agent answer&lt;/code&gt; → &lt;code&gt;chat gpt-4o-mini&lt;/code&gt; → &lt;code&gt;tool search_tool&lt;/code&gt; gave me a real conversion rate: 9.09%.&lt;/p&gt;

&lt;p&gt;And that's before the span-name fragmentation even enters. Under any single name, the LLM step can only ever match the traces of that one model, at most 10 of my 22. Now imagine the realistic production event: a fallback or a version bump quietly retires a model, its span name lingers in your funnel definition, and the step's match count goes to zero for a second reason. That's where it stopped being a quirk and became a crash.&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%2Fc09b2i77tap5bczxh9nh.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%2Fc09b2i77tap5bczxh9nh.png" alt="Funnel steps join on literal span names; a zero-match step crashes the API" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The crash
&lt;/h2&gt;

&lt;p&gt;With the third step pointed at a model that had emitted no spans at all, the funnel page went to "No data" and the conversion rate read 0.00%. The API underneath was doing something worse. The per-step endpoint (&lt;code&gt;/api/v1/trace-funnels/analytics/steps&lt;/code&gt;) still answered HTTP 200, honestly reporting &lt;code&gt;total_s3_spans: 0&lt;/code&gt;. But both overview endpoints, the ones that compute the conversion rate, returned HTTP 500 with this body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.ApiResponse.Data: []*v3.Row: v3.Row.Data: unsupported value: NaN
&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%2Fm072vjpxsnbovf8d9cvz.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%2Fm072vjpxsnbovf8d9cvz.png" alt="The verbatim request and 500 NaN response from the funnel overview endpoint" width="800" height="1104"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A step that matches nothing should be a zero-percent conversion. Zero is a perfectly good answer to "how many made it this far." Instead a zero-converting final step produces a &lt;code&gt;NaN&lt;/code&gt; the response can't carry, the request dies with a 500, and the UI papers over it as "No data." I re-verified all of this today against my live instance; the exact curl commands, request bodies, and responses are in &lt;a href="https://github.com/wiz-abhi/Signoz/blob/main/warmup-blog-2/evidence/funnel-api-repro.md" rel="noopener noreferrer"&gt;the repo&lt;/a&gt; if you want to reproduce it end to end (creating funnels needs an editor or admin token; a read-only key gets a 403).&lt;/p&gt;

&lt;p&gt;So I went reading the query builders that generate this SQL, and the shape of it is almost funny. The full-funnel query already guards its conversion division:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_s1_spans&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_sN_spans&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_s1_spans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&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;conversion_rate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That guard is why a zero-converting funnel still reports a sane 0% for conversion. But sitting in the same SELECT are two aggregates with no such protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;avgIf&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;full&lt;/span&gt; &lt;span class="n"&gt;funnel&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;&amp;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;avg_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;quantileIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;)(...,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;full&lt;/span&gt; &lt;span class="n"&gt;funnel&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;&amp;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;latency&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When no trace completes the funnel, those aggregate over an empty set, and the response carries a value JSON can't encode. The step-transition query has the same two unguarded aggregates, and its conversion division is missing the guard its sibling already has. The fix that already exists one function away is the fix the other one needs. I filed it as &lt;a href="https://github.com/SigNoz/signoz/issues/12143" rel="noopener noreferrer"&gt;SigNoz#12143&lt;/a&gt; with this repro and offered a PR.&lt;/p&gt;

&lt;p&gt;What matters for anyone using funnels today: a zero-converting step is a live crash behind a quiet UI, and GenAI span naming plus ordering makes zero-converting steps very easy to create by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works, and one thing that doesn't
&lt;/h2&gt;

&lt;p&gt;The fix that held up: wrap the LLM call in a stable parent span. If every model call lives inside an outer &lt;code&gt;llm step&lt;/code&gt; span with a constant name, you funnel on &lt;code&gt;llm step&lt;/code&gt; and the model name stops mattering to the funnel. The GenAI-named child spans stay exactly as the convention wants them, so you lose nothing.&lt;/p&gt;

&lt;p&gt;The tempting fix that doesn't work: the step filter clause. Funnel steps do accept an additional filter, and my first thought was to match on a stable attribute like &lt;code&gt;gen_ai.operation.name = "chat"&lt;/code&gt; instead of the name. But the filter narrows &lt;em&gt;within&lt;/em&gt; a span-name match rather than replacing it. A step still keys on one literal name, so no attribute filter can stitch &lt;code&gt;chat gpt-4o-mini&lt;/code&gt; and &lt;code&gt;chat gemini-3.1-flash-lite&lt;/code&gt; back into one step. Filters are still useful inside a step (scoping to one environment, say), just not for this. Knowing that saves you the hour I would have spent on it.&lt;/p&gt;

&lt;p&gt;Two operational habits, both learned the slow way. Before trusting any multi-step funnel, sanity-check each step as a single-step funnel first; that's a fast, honest span count per name, and it's how I proved my zeros were an ordering problem rather than missing data. And order your steps by when the spans actually start inside a trace, not by your mental model of the pipeline. My mental model said the LLM answers last; my traces said the LLM plans first. The funnel believed the traces.&lt;/p&gt;

&lt;p&gt;If you hit a zero of your own, this is the whole diagnosis in one picture:&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%2F1rxcrjfpdiw1b39g8ahi.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%2F1rxcrjfpdiw1b39g8ahi.png" alt="Decision flowchart: single-step funnel test separates ordering problems from name problems, and either way a zero-converting step 500s the overview API" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Trace Funnels are, for my money, one of the most underrated primitives in SigNoz for agent work. Measuring drop-off across the steps of a trace maps onto agent pipelines so naturally that I'm surprised I couldn't find it in the dedicated LLM tools. The sharp edges are specific and avoidable once you can name them: steps match literal span names, GenAI naming guarantees your most important step won't have one, ordering follows the trace rather than your intentions, and a step that converts to zero crashes the overview API instead of reporting 0%. Funnel on a wrapper span with a name you control, and check your ordering with single-step funnels first.&lt;/p&gt;

&lt;p&gt;Rough corners are normal in a young feature, and the useful response is a repro rather than a complaint. Two days with this thing and I'm more convinced it belongs in an agent developer's toolkit, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The bug: &lt;a href="https://github.com/SigNoz/signoz/issues/12143" rel="noopener noreferrer"&gt;SigNoz#12143&lt;/a&gt;, with the repro from this post&lt;/li&gt;
&lt;li&gt;Full request/response log and copy-pasteable curls: &lt;a href="https://github.com/wiz-abhi/Signoz/blob/main/warmup-blog-2/evidence/funnel-api-repro.md" rel="noopener noreferrer"&gt;funnel-api-repro.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry GenAI span conventions: &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md" rel="noopener noreferrer"&gt;gen-ai-spans.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://signoz.io/docs/" rel="noopener noreferrer"&gt;SigNoz docs&lt;/a&gt; · install path: &lt;a href="https://signoz.io/docs/install/docker/" rel="noopener noreferrer"&gt;SigNoz on Docker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;AI assistance disclosure. I used Claude as a research assistant and as an editor for structuring and tightening this post. The exploration, the self-hosted instance, the data, and the screenshots are mine. Every number was verified against my live ClickHouse and my own funnel analytics API on July 17–18, 2026; the full request/response log is &lt;a href="https://github.com/wiz-abhi/Signoz/blob/main/warmup-blog-2/evidence/funnel-api-repro.md" rel="noopener noreferrer"&gt;in the repo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>debugging</category>
    </item>
    <item>
      <title>My AI Agent Took 21 Seconds to Answer "What Is 2 + 2" — SigNoz Found Out Why in Two Clicks</title>
      <dc:creator>Abhishek Gupta</dc:creator>
      <pubDate>Sun, 12 Jul 2026 18:28:38 +0000</pubDate>
      <link>https://dev.to/abhishek_gupta_b7379457d6/my-ai-agent-took-21-seconds-to-answer-what-is-2-2-signoz-found-out-why-in-two-clicks-e3h</link>
      <guid>https://dev.to/abhishek_gupta_b7379457d6/my-ai-agent-took-21-seconds-to-answer-what-is-2-2-signoz-found-out-why-in-two-clicks-e3h</guid>
      <description>&lt;p&gt;&lt;em&gt;Warming up for the &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz hackathon&lt;/a&gt;, I self-hosted SigNoz, pointed a tiny AI agent at it, and fell hard for one feature: the one-click jump from a slow trace span into the exact logs that explain it. (2 + 2 is not supposed to be a load-bearing operation.)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — My agent answered "what is 2 + 2" in &lt;strong&gt;20.9 seconds&lt;/strong&gt;. Its own logs, read in isolation, looked perfectly healthy. In SigNoz, the trace flamegraph showed one span eating &lt;strong&gt;18.31 s — 87.62% of the request&lt;/strong&gt; — and clicking that span's &lt;strong&gt;Logs&lt;/strong&gt; tab exposed a silent retry loop that never once surfaced as an error. Two clicks from symptom to root cause. That trace↔logs correlation is my favorite feature, and this post is the story of finding it. (Then I pointed SigNoz's &lt;strong&gt;MCP server&lt;/strong&gt; at the same data and had an AI assistant re-run the whole investigation — down to the exact line number of the bug.)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why a warm-up post about an AI agent
&lt;/h2&gt;

&lt;p&gt;The hackathon's pre-event challenge — &lt;em&gt;Warm Up Before You Build&lt;/em&gt; — asks you to self-host SigNoz, send it real telemetry, explore the product, and write about your favorite feature.&lt;/p&gt;

&lt;p&gt;I could have pointed it at a hello-world web app. Everyone does. Nobody remembers the hello-world blog. But the main event is themed &lt;strong&gt;agent observability&lt;/strong&gt;, and agents have a nasty property that makes them the perfect stress test for an observability tool: &lt;strong&gt;they fail quietly&lt;/strong&gt;. No 500s, no stack traces. They loop, they retry, they burn tokens on tangents — and then return a perfectly plausible answer. The failure isn't a crash; it's &lt;em&gt;behavior&lt;/em&gt;. So my warm-up plan was simple: build a deliberately imperfect agent, wire it to SigNoz, and see how fast I could catch it misbehaving.&lt;/p&gt;

&lt;p&gt;Spoiler: faster than I expected. (Narrator: it was not, at first, faster than expected. The narrator was being generous, the same way your agent's logs are about to be generous with the truth.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting SigNoz (~15 minutes, one gotcha)
&lt;/h2&gt;

&lt;p&gt;SigNoz's current install path is &lt;strong&gt;Foundry&lt;/strong&gt;, its new installer CLI. The old flow is gone, and the docs are blunt about it: the legacy &lt;code&gt;install.sh&lt;/code&gt; script and the bundled &lt;code&gt;deploy/&lt;/code&gt; Compose files are &lt;a href="https://signoz.io/docs/install/docker/" rel="noopener noreferrer"&gt;"deprecated as of SigNoz v0.130.0 and are no longer maintained or distributed"&lt;/a&gt;. If you find a 2025 tutorial telling you to &lt;code&gt;git clone &amp;amp;&amp;amp; docker compose up&lt;/code&gt;, close the tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Docker, and a shell that can run the install script. That's genuinely it — no cloud account, no signup, no ingestion key, no sales call asking if I have "fifteen minutes to talk about my observability journey." Then three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install the foundryctl CLI&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. casting.yaml — tell Foundry to deploy via Docker Compose&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Installation&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;flavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. Cast it&lt;/span&gt;
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of minutes later, &lt;code&gt;docker ps&lt;/code&gt; showed the whole stack healthy — the SigNoz server, an OTel collector (the "ingester"), &lt;strong&gt;ClickHouse&lt;/strong&gt; for storage, ClickHouse Keeper, and a Postgres metastore. UI on &lt;code&gt;http://localhost:8080&lt;/code&gt;, OTLP ingestion on &lt;code&gt;4317&lt;/code&gt; (gRPC) and &lt;code&gt;4318&lt;/code&gt; (HTTP). Every byte of telemetry stays on my machine. (I'm on Windows 11 — Docker Desktop with the WSL2 backend, and the install script runs fine from Git Bash.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gotcha worth knowing:&lt;/strong&gt; my first OTLP exports were rejected with connection resets, and the ingester logs showed the collector stuck in an OpAMP error/restart loop. The cause was hilariously mundane — I hadn't created the admin account yet. SigNoz's collector gets its config via OpAMP from the server, and until the first user/org exists it has nothing to serve. The moment I signed up at &lt;code&gt;localhost:8080&lt;/code&gt;, the collector settled and ingestion turned green. If your self-hosted collector seems broken on first boot: &lt;strong&gt;create your account first.&lt;/strong&gt; (It is, once again, always the auth. It has never once not been the auth.)&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%2Fyscim4stwpufvkbr3195.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%2Fyscim4stwpufvkbr3195.png" alt="SigNoz Services page showing warmup-agent with P99 latency of 21,818 ms" width="798" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Services page a few minutes later: one application, &lt;code&gt;warmup-agent&lt;/code&gt;, P99 latency &lt;strong&gt;21,818 ms&lt;/strong&gt; — derived automatically from its spans, no metrics code written. For "2 + 2". Most pocket calculators clear that in under a second, batteries included. We'll get to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent, and how it's instrumented
&lt;/h2&gt;

&lt;p&gt;My agent is a single Python file doing the classic loop real frameworks do — plan with an LLM (Gemini's &lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;, via its OpenAI-compatible endpoint), call a tool, answer with an LLM:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plan how to answer: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# LLM call #1
&lt;/span&gt;    &lt;span class="n"&gt;facts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;# tool call
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; using: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="c1"&gt;# LLM call #2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For instrumentation I went OTel-native and hand-rolled spans following the &lt;strong&gt;OpenTelemetry GenAI semantic conventions&lt;/strong&gt;, the vendor-neutral standard (&lt;code&gt;gen_ai.*&lt;/code&gt; attributes) for recording model names, token usage, agent steps, and tool calls. One thing that cost me ten minutes of confused searching: these conventions &lt;strong&gt;moved out of the main &lt;code&gt;semantic-conventions&lt;/code&gt; repo&lt;/strong&gt; into their own home at &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;&lt;code&gt;open-telemetry/semantic-conventions-genai&lt;/code&gt;&lt;/a&gt;, and the old &lt;code&gt;opentelemetry.io/docs/specs/semconv/gen-ai/&lt;/code&gt; URL is now just a "this has moved" stub. The two pages you actually want are &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md" rel="noopener noreferrer"&gt;gen-ai-spans.md&lt;/a&gt; for model calls and &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-agent-spans.md" rel="noopener noreferrer"&gt;gen-ai-agent-spans.md&lt;/a&gt; for agents and tool execution. They're still experimental, so expect attribute names to shift under you.&lt;/p&gt;

&lt;p&gt;Every LLM call becomes a span carrying its contract with the outside world:&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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="si"&gt;}&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.operation.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MODEL&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.agent.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warmup-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... make the call ...
&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_tok&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_tok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two exporters point at my local SigNoz — &lt;code&gt;OTLPSpanExporter&lt;/code&gt; for traces and &lt;code&gt;OTLPLogExporter&lt;/code&gt; behind a stock &lt;code&gt;logging&lt;/code&gt; handler for logs — both to &lt;code&gt;http://localhost:4318&lt;/code&gt;. That logging handler is the quiet hero of this story: it stamps every log record emitted inside a span with that span's &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;. Remember that detail.&lt;/p&gt;

&lt;p&gt;(If you'd rather not write spans by hand: &lt;code&gt;opentelemetry-instrument&lt;/code&gt; plus the GenAI instrumentation packages will auto-capture the same &lt;code&gt;gen_ai.*&lt;/code&gt; attributes for OpenAI, LangChain, and friends with zero code changes. I wanted to feel the semantic conventions under my fingers at least once.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The mystery: 21 seconds for "2 + 2"
&lt;/h2&gt;

&lt;p&gt;First real runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run 1: arithmetic: 2 + 2 = 4 (source: math)  (21.9s)
run 2: Arithmetic: 2 + 2 = 4 (source: math)  (20.6s)
run 3: arithmetic: 2 + 2 = 4 (source: math)  (20.9s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct answer. Absurd latency.&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%2F82brx27yd11ghb6mlbua.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%2F82brx27yd11ghb6mlbua.png" alt="Expectation vs reality: 2+2=4 in under a second vs. 20.9 seconds with three silent retries" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first guess was the lazy one: free-tier Gemini being free-tier Gemini. I'd already fought its rate limits earlier that day (the retry delay in my run loop exists for a reason), so a slow API felt plausible enough that I almost didn't investigate. Step one of the Five Stages of Ignoring a Bug is always "blame the free tier." It is never the free tier's fault quite as often as we'd like it to be.&lt;/p&gt;

&lt;p&gt;Two things stopped me. Twenty seconds is slow even for a free tier having a bad day. And scrolling my terminal logs, nothing looked &lt;em&gt;wrong&lt;/em&gt; — no errors, no exceptions, a clean INFO trail, the software equivalent of a suspect saying "no further questions, officer" with total confidence. This is the agent failure mode in miniature: everything "works," and something is deeply off.&lt;/p&gt;

&lt;p&gt;Time to look at what SigNoz saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the flamegraph: where did 19.5 seconds go?
&lt;/h2&gt;

&lt;p&gt;I opened the &lt;strong&gt;Traces Explorer&lt;/strong&gt;, and rather than scrolling, made the query builder do the work — one filter, &lt;code&gt;durationNano &amp;gt;= 15s&lt;/code&gt;, and only the pathological runs remained. Clicked one.&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%2Fgnugjeayt26yt4lodxo1.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%2Fgnugjeayt26yt4lodxo1.png" alt="Trace flamegraph: agent answer 20.89s with tool search_tool consuming 18.31s" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One glance killed my Gemini theory: the root &lt;code&gt;agent answer&lt;/code&gt; span is 20.89 s, and sitting under it is a monstrous &lt;code&gt;tool search_tool&lt;/code&gt; bar — &lt;strong&gt;18.31 s, 87.62% of total execution time&lt;/strong&gt;. The two &lt;code&gt;chat gemini-3.1-flash-lite&lt;/code&gt; spans flanking it are slivers (1.57 s for planning, a blink for the answer). The LLM I'd been ready to blame was never the problem; my &lt;em&gt;tool&lt;/em&gt; was. The span's attributes (that's the GenAI conventions paying off) confirmed it: &lt;code&gt;gen_ai.tool.name: "search_tool"&lt;/code&gt;, and — suspicious — &lt;code&gt;tool.attempts: 3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's called a &lt;strong&gt;flame&lt;/strong&gt;graph. I did not expect that name to be quite this on the nose — 87.62% of my request was, functionally, on fire, and my terminal logs were standing next to it holding a cup of coffee insisting everything was fine:&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%2Fswi3x5o00hezxlb62qel.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%2Fswi3x5o00hezxlb62qel.png" alt="My agent's logs during the entire 18.3-second incident, insisting everything is fine" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A trace tells you &lt;strong&gt;where&lt;/strong&gt; the time went. It doesn't tell you &lt;strong&gt;why&lt;/strong&gt;. Historically, "why" is where debugging gets miserable: copy the timestamp, switch to your logging tool, paste an approximate time window, squint at clock skew, guess which lines belong to this exact request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature that won me over: falling from a span into its logs
&lt;/h2&gt;

&lt;p&gt;In SigNoz, the span details panel has a &lt;strong&gt;Logs&lt;/strong&gt; tab. I clicked it with the slow span selected.&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%2F538ovb52zrbdqmzaq76b.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%2F538ovb52zrbdqmzaq76b.png" alt="The slow span selected with its correlated retry warning logs shown in the Logs tab" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The money shot. There they were. Not "logs from around that time" — &lt;em&gt;the logs of that exact span&lt;/em&gt;, joined by the &lt;code&gt;trace_id&lt;/code&gt; the instrumentation had been stamping all along, with the two WARN lines glowing amber in the panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING search_tool: upstream timeout, retrying (1/3)
WARNING search_tool: upstream timeout, retrying (2/3)
INFO    search_tool: succeeded on attempt 3
&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%2Fha833vuygv4azh81f651.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%2Fha833vuygv4azh81f651.png" alt="Logs Explorer filtered by trace_id showing the full narrative of one agent run" width="799" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same story in the Logs Explorer, filtered to &lt;code&gt;trace_id = '359c33c37996f84f6cf2fc02ffd5ab03'&lt;/code&gt;: the full narrative of one agent run, from "received question" through the auto-captured &lt;code&gt;httpx&lt;/code&gt; lines of the actual Gemini API calls, to the retry loop, to the answer — each LLM step even reporting its token usage (&lt;code&gt;llm plan completed (14 in / 196 out tokens)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And there was my bug, in three lines: my search tool had a &lt;strong&gt;silent retry loop&lt;/strong&gt; — a 3 s timeout and an aggressive exponential backoff against a flaky upstream. It always recovered by attempt three, so no error ever surfaced. The agent "worked." The user just waited 21 seconds. Plot twist: the villain was never Gemini. The villain lived in &lt;code&gt;search_tool.py&lt;/code&gt;, three retries deep, wearing an INFO-level trenchcoat. Only the &lt;em&gt;combination&lt;/em&gt; — trace for where, logs for why, welded by a shared ID — told the whole story, in roughly two clicks and zero timestamp archaeology.&lt;/p&gt;

&lt;p&gt;The fix was three constants (cap the retries, shrink the backoff, fall back to cache fast). Next runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run 1: arithmetic: 2 + 2 = 4 (cached)  (3.6s)
run 2: arithmetic: 2 + 2 = 4 (cached)  (2.6s)
run 3: arithmetic: 2 + 2 = 4 (cached)  (2.7s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;~21 s → ~2.6 s.&lt;/strong&gt; But the fix isn't the point. The two clicks are the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works: one database, not three
&lt;/h2&gt;

&lt;p&gt;This isn't UI sugar — it falls out of an architectural decision, and it's worth understanding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Context propagation.&lt;/strong&gt; OpenTelemetry stamps every log emitted inside a span with &lt;code&gt;trace_id&lt;/code&gt;/&lt;code&gt;span_id&lt;/code&gt;. The link lives in the data itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One datastore.&lt;/strong&gt; SigNoz keeps traces, logs, &lt;em&gt;and&lt;/em&gt; metrics in the same ClickHouse database. "Logs for this span" is a single indexed query — not a federated join across a trace store, a log store, and a metrics store with three query languages and three clocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One query engine.&lt;/strong&gt; The same query builder speaks to all three signals, so correlation is a first-class action rather than a mental join you perform across browser tabs.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Stitched-together stack (Tempo + Loki + Prometheus)&lt;/th&gt;
&lt;th&gt;SigNoz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trace → logs&lt;/td&gt;
&lt;td&gt;Copy timestamp, switch tool, guess the window&lt;/td&gt;
&lt;td&gt;Click the span's &lt;strong&gt;Logs&lt;/strong&gt; tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query languages&lt;/td&gt;
&lt;td&gt;TraceQL + LogQL + PromQL&lt;/td&gt;
&lt;td&gt;One query builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock skew between signals&lt;/td&gt;
&lt;td&gt;Your problem&lt;/td&gt;
&lt;td&gt;Same store, same clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to "why"&lt;/td&gt;
&lt;td&gt;Minutes, if you're good&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I've done the copy-the-timestamp dance for years. I didn't register how much of it was pure tax until SigNoz deleted it — turns out I'd been paying rent to three different landlords (Tempo, Loki, Prometheus) who don't talk to each other and definitely don't split a security deposit.&lt;/p&gt;

&lt;p&gt;And the rest of the product hangs off the same spine, which is what makes the exploration feel coherent rather than like five bolted-together tools: the &lt;strong&gt;Services&lt;/strong&gt; page derived RED metrics (P99, error rate, ops/sec) for my agent purely from its spans; the &lt;strong&gt;Logs Explorer&lt;/strong&gt; runs fast structured queries over the same store (severity and trace filters, plus one-click &lt;strong&gt;Create an Alert&lt;/strong&gt; / &lt;strong&gt;Add to Dashboard&lt;/strong&gt; straight from any query); and because dashboards and alerts are built on the identical query builder, the p95-latency alert for this agent is the exact query I already wrote, reused.&lt;/p&gt;

&lt;p&gt;To prove that to myself, I finished by building an &lt;strong&gt;Agent Health&lt;/strong&gt; dashboard — four panels, each just a query-builder expression: agent runs over time (&lt;code&gt;count()&lt;/code&gt; on the root span), p95 agent latency (&lt;code&gt;p95(duration_nano)&lt;/code&gt;), LLM output tokens (&lt;code&gt;sum(gen_ai.usage.output_tokens)&lt;/code&gt; — the GenAI conventions again), and tool retry warnings (&lt;code&gt;count()&lt;/code&gt; on WARN logs). Four different questions, three different signals, one query language.&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%2Ftnpwbkjisrncjepbppfn.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%2Ftnpwbkjisrncjepbppfn.png" alt="Agent Health dashboard: runs, p95 latency, LLM output tokens, and retry warnings" width="800" height="828"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read the top-right and bottom-right panels together and the whole incident is visible at a glance: p95 latency pinned above 20 s exactly while the retry-warning counts spike, then both collapse after the fix. That's the correlation thesis of this post, drawn as two lines — and it's four panels, zero SQL, and a distinct lack of me googling "how do I join traces and logs by timestamp" at 1 a.m., possibly ever again.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing: I let an AI agent read my telemetry
&lt;/h2&gt;

&lt;p&gt;SigNoz &lt;a href="https://signoz.io/docs/ai/signoz-mcp-server/" rel="noopener noreferrer"&gt;shipped an official &lt;strong&gt;MCP server&lt;/strong&gt;&lt;/a&gt; — a Model Context Protocol bridge that lets any MCP-compatible AI assistant (Claude Code, Cursor, Gemini CLI…) query your observability data with natural tool calls. Since this hackathon is literally called &lt;em&gt;Agents of SigNoz&lt;/em&gt;, I couldn't resist closing the loop: an AI agent debugging my AI agent.&lt;/p&gt;

&lt;p&gt;Setup was one Docker command plus a &lt;strong&gt;read-only service account&lt;/strong&gt; (&lt;a href="https://signoz.io/docs/manage/administrator-guide/iam/service-accounts/" rel="noopener noreferrer"&gt;Settings → Service Accounts&lt;/a&gt; — the MCP server only needs to read):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TRANSPORT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;SIGNOZ_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://host.docker.internal:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;SIGNOZ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;service-account-key&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  signoz/signoz-mcp-server:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The gotcha that cost me half an hour:&lt;/strong&gt; I created the service account through the API (&lt;code&gt;POST /api/v1/service_accounts&lt;/code&gt;) and passed a &lt;code&gt;role&lt;/code&gt; field. The call returned &lt;code&gt;201 Created&lt;/code&gt; and the role was silently dropped on the floor — so every single MCP call came back &lt;code&gt;403&lt;/code&gt; with &lt;em&gt;"only viewers/editors/admins"&lt;/em&gt;, which reads like a key problem when it's actually a role problem. I spent a genuinely embarrassing amount of that half hour convinced my API key was cursed. It was not cursed. It was roleless, which, philosophically, might be worse. The fix isn't in the request body: open the account in &lt;strong&gt;Settings → Service Accounts → Roles&lt;/strong&gt;, add &lt;strong&gt;&lt;code&gt;signoz-viewer&lt;/code&gt;&lt;/strong&gt;, save. Then it works. If you're 403ing with a key you're certain is correct, go look at the role in the UI.&lt;/p&gt;

&lt;p&gt;How many tools does it expose? The docs currently say 33. The &lt;code&gt;tools/list&lt;/code&gt; call against the &lt;code&gt;signoz/signoz-mcp-server:latest&lt;/code&gt; image I pulled returned &lt;strong&gt;41&lt;/strong&gt;: &lt;code&gt;signoz_list_services&lt;/code&gt;, &lt;code&gt;signoz_search_traces&lt;/code&gt;, &lt;code&gt;signoz_search_logs&lt;/code&gt;, &lt;code&gt;signoz_query_metrics&lt;/code&gt;, even &lt;code&gt;signoz_create_alert&lt;/code&gt; and &lt;code&gt;signoz_create_dashboard&lt;/code&gt;. This thing ships fast enough that any count in a blog post, this one included, is a snapshot. Ask your own instance rather than trusting my number.&lt;/p&gt;

&lt;p&gt;I connected it to Claude Code and watched it re-run my entire investigation without touching the UI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"What services do I have?"&lt;/strong&gt; → &lt;code&gt;signoz_list_services&lt;/code&gt;: one service, &lt;code&gt;warmup-agent&lt;/code&gt;, 22 calls, &lt;strong&gt;p99 21.7 s&lt;/strong&gt;, zero errors. (Zero errors! The agent is "healthy," remember?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Show me the slowest agent runs"&lt;/strong&gt; → &lt;code&gt;signoz_search_traces&lt;/code&gt; with &lt;code&gt;duration_nano &amp;gt; 15s&lt;/code&gt;: trace &lt;code&gt;359c33c3…&lt;/code&gt;, 20.89 s — the exact trace from the flamegraph above, returned with a clickable &lt;code&gt;webUrl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Any warnings?"&lt;/strong&gt; → &lt;code&gt;signoz_search_logs&lt;/code&gt; with &lt;code&gt;severity_text = 'WARN'&lt;/code&gt;: the retry-loop warnings — and because OTel logging captures code attributes, each log came back with &lt;code&gt;code.function.name: "search_tool"&lt;/code&gt; and &lt;code&gt;code.line.number: 122&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read that last one again: the assistant didn't just find the warning — the telemetry told it &lt;strong&gt;which function and which line of my source code produced it&lt;/strong&gt;. An AI agent read my observability data and pointed at line 122 of &lt;code&gt;agent.py&lt;/code&gt;. My own code, ratted out by my own telemetry, to my own AI assistant. There's a lesson in there about accountability that I've chosen not to examine too closely. That's the "agent-native observability" pitch, working end-to-end on my laptop, against a self-hosted instance, with a read-only key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters double for agents
&lt;/h2&gt;

&lt;p&gt;Here's the thesis I'm carrying into the hackathon: &lt;strong&gt;for AI agents, signal correlation isn't a nice-to-have — it's the whole game.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent run &lt;em&gt;is&lt;/em&gt; a trace: plan step, tool calls, reflection, answer — a tree of spans. The GenAI attributes annotate each step with model, tokens, and soon cost. The correlated logs record what each tool actually did. Put together, a single trace turns a non-deterministic black box into a &lt;em&gt;readable story of one decision&lt;/em&gt;. My 18.3-second retry loop is the tame version; the same two-click move catches the expensive stuff — the agent that calls the same tool four times, the prompt that ballooned to 30k tokens, the tool that quietly falls back to stale cache.&lt;/p&gt;

&lt;p&gt;Traditional services fail loudly. Agents fail politely. You need the trace to notice, and the logs to understand.&lt;/p&gt;

&lt;p&gt;Three things I now believe in the way you believe things after they've personally embarrassed you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"No errors in the logs" is not the same claim as "nothing is wrong." It is the claim "nothing threw an exception," which is a much smaller and much less comforting claim.&lt;/li&gt;
&lt;li&gt;If your span is named after fire, maybe check that one first.&lt;/li&gt;
&lt;li&gt;Retries are like glitter: a little bit hides a lot of problems, and you will regret using them within the hour.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;Everything above is one repo and about five minutes. You need Docker and Python 3.11+; an LLM key is optional, because the agent falls back to a local stub with realistic latency if it doesn't find one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Self-host SigNoz&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
git clone https://github.com/wiz-abhi/Signoz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;Signoz
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; warmup-agent/casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open &lt;code&gt;http://localhost:8080&lt;/code&gt; and &lt;strong&gt;create your admin account before doing anything else&lt;/strong&gt; — that's the OpAMP gotcha from earlier. Skip this and step 3 fails with connection resets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. Install the agent's dependencies&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;warmup-agent
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
.venv/Scripts/pip &lt;span class="nb"&gt;install &lt;/span&gt;opentelemetry-sdk opentelemetry-exporter-otlp-proto-http openai  &lt;span class="c"&gt;# Linux/macOS: .venv/bin/pip&lt;/span&gt;

&lt;span class="c"&gt;# 3. Run it — buggy first, then fixed&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...                    &lt;span class="c"&gt;# optional; omit to use the stub&lt;/span&gt;
.venv/Scripts/python agent.py &lt;span class="nt"&gt;--runs&lt;/span&gt; 3       &lt;span class="c"&gt;# the slow version, ~21s per answer&lt;/span&gt;
.venv/Scripts/python agent.py &lt;span class="nt"&gt;--fixed&lt;/span&gt; &lt;span class="nt"&gt;--runs&lt;/span&gt; 5  &lt;span class="c"&gt;# after the fix, ~2.6s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the Traces Explorer, filter &lt;code&gt;durationNano &amp;gt;= 15s&lt;/code&gt;, click the fat &lt;code&gt;tool search_tool&lt;/code&gt; span, and hit its &lt;strong&gt;Logs&lt;/strong&gt; tab. That's the whole post, replayed on your machine in the same two clicks.&lt;/p&gt;

&lt;p&gt;Two things that will bite you on Windows specifically: run the Foundry install script from &lt;strong&gt;Git Bash&lt;/strong&gt;, not PowerShell (it's a bash script), and use &lt;code&gt;.venv/Scripts/&lt;/code&gt; where the snippets above say &lt;code&gt;.venv/bin/&lt;/code&gt;. The &lt;code&gt;--fixed&lt;/code&gt; flag only moves three constants in &lt;a href="https://github.com/wiz-abhi/Signoz/blob/main/warmup-agent/agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent.py&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;max_retries&lt;/code&gt;, &lt;code&gt;base_backoff&lt;/code&gt;, and &lt;code&gt;timeout&lt;/code&gt; — so you can watch the flamegraph collapse in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building for the main event
&lt;/h2&gt;

&lt;p&gt;This warm-up is becoming my Track 1 foundation: a multi-step, tool-calling agent with observability designed in from line one — per-step token and cost tracking via the GenAI conventions, alerts on runaway loops and token spikes, and a dashboard where every panel drills down to the exact trace (and therefore the exact logs) behind it. And now that I've seen the MCP server work, the loop I really want to build is &lt;em&gt;self-referential&lt;/em&gt;: an agent that watches its own SigNoz telemetry and adjusts its behavior when it sees itself misbehaving. Whether I can pull that off in a week is an open question. That's what the week is for.&lt;/p&gt;

&lt;p&gt;If you're on the fence about the hackathon: &lt;code&gt;foundryctl cast&lt;/code&gt;, point anything you're building at &lt;code&gt;localhost:4318&lt;/code&gt;, open your slowest trace, and click the span's &lt;strong&gt;Logs&lt;/strong&gt; tab. That two-second fall from &lt;em&gt;where&lt;/em&gt; to &lt;em&gt;why&lt;/em&gt; is the moment "three pillars of observability" stops being a conference slide and becomes a debugging reflex.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Everything here ran on my laptop: self-hosted SigNoz v0.132 via Foundry, OpenTelemetry Python SDK, Gemini (&lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;) as the agent's brain, the SigNoz MCP server in Docker, and a ~140-line agent. Code, scripts, and all screenshots: &lt;a href="https://github.com/wiz-abhi/Signoz" rel="noopener noreferrer"&gt;github.com/wiz-abhi/Signoz&lt;/a&gt;. Written for the &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz&lt;/a&gt; warm-up challenge by &lt;a href="https://x.com/wemakedevs" rel="noopener noreferrer"&gt;@wemakedevs&lt;/a&gt; and &lt;a href="https://signoz.io" rel="noopener noreferrer"&gt;SigNoz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI assistance disclosure.&lt;/strong&gt; I used Claude as a coding assistant while building the agent and as an editor while structuring this post. Everything it describes is mine and actually happened: I ran the stack, hit the bugs, took every screenshot from my own SigNoz instance, and verified each claim against the live system or the linked docs before it went in. The trace IDs and numbers are real — you can reproduce them with the steps above. Claude also appears &lt;em&gt;in&lt;/em&gt; the story, as the MCP client in the last section; that part was the fun of it.&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building Kavach: catching fraud rings, not just fraudsters — with Neo4j</title>
      <dc:creator>Abhishek Gupta</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:03:55 +0000</pubDate>
      <link>https://dev.to/abhishek_gupta_b7379457d6/building-kavach-catching-fraud-rings-not-just-fraudsters-with-neo4j-h0h</link>
      <guid>https://dev.to/abhishek_gupta_b7379457d6/building-kavach-catching-fraud-rings-not-just-fraudsters-with-neo4j-h0h</guid>
      <description>&lt;p&gt;&lt;strong&gt;▶ Live demo:&lt;/strong&gt; &lt;a href="https://kavach-woad-beta.vercel.app" rel="noopener noreferrer"&gt;https://kavach-woad-beta.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;💻 Code:&lt;/strong&gt; &lt;a href="https://github.com/wiz-abhi/Kavach" rel="noopener noreferrer"&gt;https://github.com/wiz-abhi/Kavach&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;🎥 Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/EZZLWpmqrS0" rel="noopener noreferrer"&gt;https://youtu.be/EZZLWpmqrS0&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Traditional fraud detection scores one account at a time and misses organized &lt;em&gt;rings&lt;/em&gt;. I modeled accounts, devices, IPs, phones, and transactions as a graph in &lt;strong&gt;Neo4j AuraDB&lt;/strong&gt;, detected rings as dense infrastructure-sharing clusters with native Cypher, and got &lt;strong&gt;100% recall / 0 false positives&lt;/strong&gt; against ground truth. Then I made the "why a graph database?" argument &lt;em&gt;provable&lt;/em&gt; by running the same query in Cypher vs. SQL, live.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/EZZLWpmqrS0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem that started it
&lt;/h2&gt;

&lt;p&gt;Every fraud system I'd read about asks the same question: &lt;em&gt;"Is **this&lt;/em&gt;* account suspicious?"* It looks at one account's transaction amount, its velocity, its location, and scores it in isolation.&lt;/p&gt;

&lt;p&gt;That works for lone fraudsters. It completely misses the expensive kind of fraud: &lt;strong&gt;organized rings&lt;/strong&gt;. A ring is a group of accounts that quietly share infrastructure — the same device fingerprint, the same IP, the same phone number — and cycle money between themselves to launder it or farm signup bonuses and loans. No single account looks that bad. The &lt;em&gt;pattern between them&lt;/em&gt; is the crime.&lt;/p&gt;

&lt;p&gt;And that pattern is exactly what a row-and-column database is worst at. "Which accounts are secretly connected, and how deep does the ring go?" becomes a nightmare of self-joins in SQL. But it's the single most natural thing in the world for a graph.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Kavach&lt;/strong&gt; (Sanskrit for &lt;em&gt;shield&lt;/em&gt;): model accounts, devices, IPs, phones and transactions as a graph in &lt;strong&gt;Neo4j AuraDB&lt;/strong&gt;, then detect the rings as dense, infrastructure-sharing clusters — and explain &lt;em&gt;why&lt;/em&gt; each one was flagged, in plain English.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: shared infrastructure is an edge
&lt;/h2&gt;

&lt;p&gt;The whole thing hinges on one modeling decision. In Kavach, a device isn't a column on an account — it's its own node. When two accounts use the same device, they're two hops apart in the graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ACC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:USED_DEVICE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEV&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:USED_DEVICE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ACC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly "find accounts that share infrastructure" is a graph pattern, and "trace the whole ring" is a variable-length traversal. Detection becomes: find clusters of accounts linked through shared devices/IPs/phones &lt;strong&gt;that also transact densely with each other&lt;/strong&gt; — because sharing a device with your spouse is innocent; sharing a device &lt;em&gt;and&lt;/em&gt; moving money in a tight loop is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built
&lt;/h2&gt;

&lt;p&gt;Three services around one AuraDB instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generator&lt;/strong&gt; — synthetic accounts + deliberately seeded fraud rings, with a &lt;code&gt;ground_truth.json&lt;/code&gt; so I could &lt;em&gt;measure&lt;/em&gt; whether detection actually works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt; (Express + &lt;code&gt;neo4j-driver&lt;/code&gt;) — detection, a live WebSocket feed, and the features below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; (Next.js) — a dark "security ops" dashboard with a live force-directed graph, ring alerts, and the interactive tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The moment the detection number went from "meh" to "wow"
&lt;/h2&gt;

&lt;p&gt;My first detection run caught &lt;strong&gt;1 of 4&lt;/strong&gt; seeded rings. Not great.&lt;/p&gt;

&lt;p&gt;The bug was subtle and worth sharing: I was scoring ring "density" as &lt;code&gt;internal_transactions / ring_size&lt;/code&gt; and comparing it to a per-account average. That metric &lt;em&gt;punishes bigger rings&lt;/em&gt; — a 9-account ring looked less dense than a 6-account one, even when both were equally coordinated.&lt;/p&gt;

&lt;p&gt;The fix was to measure &lt;strong&gt;transaction edge density relative to the graph's random baseline&lt;/strong&gt;: of all possible pairs inside the cluster, how many actually transact, versus how often any two random accounts in the network transact. That number is size-independent and brutally discriminating. The seeded rings came back at &lt;strong&gt;63× to 144×&lt;/strong&gt; the baseline; the detection threshold sits at 5×.&lt;/p&gt;

&lt;p&gt;Result, verified against ground truth: &lt;strong&gt;100% recall, 0 false positives.&lt;/strong&gt; I wired that check into &lt;code&gt;npm run validate&lt;/code&gt; so I can prove it live instead of just asserting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the "why graph?" case &lt;em&gt;provable&lt;/em&gt;, not just claimed
&lt;/h2&gt;

&lt;p&gt;Judges on a database track have heard "graph is better for this" a hundred times. I wanted to &lt;em&gt;show&lt;/em&gt; it. So Kavach has a &lt;strong&gt;"Why Graph?" panel&lt;/strong&gt; that runs the identical fraud query two ways on the same live data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Cypher&lt;/strong&gt;, against Neo4j.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;SQL&lt;/strong&gt;, against an in-memory SQLite mirror of the same graph, built on the fly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It displays them side by side and confirms the result sets match. On a single hop they're comparable — but the honest, interesting part is the &lt;em&gt;transitive&lt;/em&gt; query ("trace the entire ring"): in Cypher it's one variable-length pattern; in SQL it's a recursive CTE that gets heavier with every hop. Same answer, very different query. That's the whole argument for a graph database, made concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two features that turn a red flag into an investigation
&lt;/h2&gt;

&lt;p&gt;Detecting a ring is step one. A real analyst needs to &lt;em&gt;act&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Investigate&lt;/strong&gt; uses Neo4j &lt;code&gt;shortestPath&lt;/code&gt; to answer "how are these two accounts secretly connected?" In "shared-identity" mode it ignores transactions and surfaces the hidden chain — e.g. &lt;em&gt;ACC-402 shares an IP with ACC-401, who shares a device with ACC-406&lt;/em&gt; — a link between accounts that never once transacted directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyst Copilot&lt;/strong&gt; lets you ask in plain English ("How much money moved inside the rings?") and answers with a real, read-only Cypher query it generates and runs. It uses an LLM when configured, but falls back to a deterministic rule-based translator so a live demo can never be broken by an external API being down — and it shows you the Cypher every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons from shipping it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never trust an unrun build.&lt;/strong&gt; The scaffolding "looked" done, but the graph endpoint was passing a float to &lt;code&gt;LIMIT&lt;/code&gt; (which Neo4j rejects) and a stats query had a Cartesian product reporting &lt;em&gt;545,292&lt;/em&gt; flagged accounts. Both would have died on stage. Run everything, end to end, before you believe it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure your own detection.&lt;/strong&gt; Seeding ground truth and reporting recall/precision turned a vague "it finds rings" into "4/4, 0 false positives." That one number is the most convincing thing in the whole demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design your demo to not depend on the internet.&lt;/strong&gt; Every impressive-but-fragile feature got a reliable fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Kavach runs entirely on the &lt;strong&gt;Neo4j AuraDB free tier&lt;/strong&gt; — detection deliberately avoids the GDS plugin (unavailable on free) so anyone can spin up an instance and run it. Full setup, API reference, and a 3-minute demo script are in the &lt;a href="https://github.com/wiz-abhi/Kavach" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Built for &lt;strong&gt;HACKHAZARDS '26&lt;/strong&gt; by &lt;a href="https://www.namespace.world/" rel="noopener noreferrer"&gt;NAMESPACE&lt;/a&gt;, on the &lt;strong&gt;Neo4j AuraDB&lt;/strong&gt; track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch the ring, not just the account.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>neo4j</category>
      <category>hackathon</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I gave my AI agent's memory a CI/CD pipeline</title>
      <dc:creator>Abhishek Gupta</dc:creator>
      <pubDate>Sun, 05 Jul 2026 18:16:09 +0000</pubDate>
      <link>https://dev.to/abhishek_gupta_b7379457d6/i-gave-my-ai-agents-memory-a-cicd-pipeline-56hf</link>
      <guid>https://dev.to/abhishek_gupta_b7379457d6/i-gave-my-ai-agents-memory-a-cicd-pipeline-56hf</guid>
      <description>&lt;h1&gt;
  
  
  🧠 I gave my AI agent's memory a CI/CD pipeline
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Building &lt;strong&gt;SOBER&lt;/strong&gt; — tests, diff, bisect, and gated deploys for a knowledge graph — for the Cognee × WeMakeDevs "Where's My Context?" hackathon.
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Your agent's memory is production infrastructure with &lt;em&gt;no tests, no diff, no rollback.&lt;/em&gt; SOBER is a &lt;code&gt;brain&lt;/code&gt; CLI + GitHub Action that wraps a &lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt; knowledge graph in real CI/CD: &lt;strong&gt;forget-regression tests&lt;/strong&gt; that prove a retracted secret stays gone, &lt;strong&gt;&lt;code&gt;git bisect&lt;/code&gt; for a poisoned graph&lt;/strong&gt;, and a nightly &lt;code&gt;improve()&lt;/code&gt; that opens its own pull request behind a green eval gate. Built solo, AI-assisted, and — the fun part — reviewed by a fleet of agents that found &lt;strong&gt;14 real bugs&lt;/strong&gt; in my own code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🍸 The 3 a.m. problem
&lt;/h2&gt;

&lt;p&gt;Every on-call engineer knows the feeling the hackathon is named after: it's 3 a.m., something is broken, and &lt;em&gt;nobody remembers how it got fixed last time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We solved that for &lt;strong&gt;code&lt;/strong&gt; decades ago — version control, tests, code review, rollback, canary deploys. But your AI agent's &lt;strong&gt;memory&lt;/strong&gt;, which increasingly decides what your agent knows and does, ships with none of it. It mutates in place, silently. There is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ no &lt;strong&gt;test&lt;/strong&gt; to stop a retracted secret or a stale, dangerous fact from staying recallable&lt;/li&gt;
&lt;li&gt;❌ no &lt;strong&gt;diff&lt;/strong&gt; to see what a re-ingest or an &lt;code&gt;improve()&lt;/code&gt; run changed in the graph&lt;/li&gt;
&lt;li&gt;❌ no &lt;strong&gt;bisect&lt;/strong&gt; to find &lt;em&gt;which&lt;/em&gt; ingestion poisoned the brain&lt;/li&gt;
&lt;li&gt;❌ no &lt;strong&gt;gate&lt;/strong&gt; before memory "ships" to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;SOBER — CI/CD for Agent Brains.&lt;/strong&gt; Not memory &lt;em&gt;for&lt;/em&gt; DevOps. &lt;strong&gt;DevOps &lt;em&gt;for&lt;/em&gt; memory.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Picking the idea (the honest part)
&lt;/h2&gt;

&lt;p&gt;I didn't land on SOBER first. My initial instinct was an &lt;em&gt;incident-memory copilot&lt;/em&gt; — an on-call assistant that recalls past outages. It felt strong until I pressure-tested it against the audience:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WeMakeDevs is a &lt;strong&gt;DevOps community&lt;/strong&gt;, so "3 a.m. incident memory" is the single most &lt;em&gt;predictable&lt;/em&gt; thing to build for them — and to Cognee's own engineers it reads as their past "Company Brain" hackathon winner with the nouns swapped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It would have drowned in look-alikes. The winning reframe came from a different question: not &lt;em&gt;"what memory app should I build?"&lt;/em&gt; but &lt;em&gt;"what does memory-the-category still lack?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer: the entire &lt;strong&gt;operations layer&lt;/strong&gt;. Every other entry would &lt;em&gt;use&lt;/em&gt; Cognee to remember things. SOBER governs the remembering itself — and in doing so it leans hard on the one Cognee verb nobody demos: &lt;strong&gt;&lt;code&gt;forget()&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture: a brain is a &lt;em&gt;family&lt;/em&gt; of datasets
&lt;/h2&gt;

&lt;p&gt;The load-bearing design decision. Cognee's &lt;code&gt;forget()&lt;/code&gt; is scoped to a whole dataset, but I needed to retract &lt;strong&gt;one batch&lt;/strong&gt; of knowledge without disturbing the rest. So a &lt;em&gt;logical&lt;/em&gt; brain is modeled as a &lt;strong&gt;family&lt;/strong&gt; of physical datasets — one per ingestion batch (&lt;code&gt;node_set&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    K["📄 knowledge/*.md"] --&amp;gt;|brain build| ING["ingest_batch()"]
    ING --&amp;gt; C["brain__core"]
    ING --&amp;gt; R["brain__runbooks"]
    ING --&amp;gt; X["brain__retracted 🔴"]

    subgraph BRAIN["🧠 logical brain = union of the family"]
        C
        R
        X
    end

    BRAIN --&amp;gt;|"CHUNKS recall (local, no LLM)"| EV["🧪 memory CI evals"]
    BRAIN --&amp;gt;|"export + merge"| SNAP["📸 snapshot vN (JSON)"]
    SNAP --&amp;gt;|diff| DIFF["🔀 graph diff"]
    X -.-&amp;gt;|"forget(node_set='retracted')"| GONE["💨 surgically removed"]

    EV --&amp;gt; GATE{"🟢/🔴 gate"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;recall()&lt;/code&gt; and &lt;code&gt;export()&lt;/code&gt; span the whole family&lt;/strong&gt; → knowledge is found no matter which batch holds it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;forget(node_set)&lt;/code&gt; drops exactly one member&lt;/strong&gt; → surgical retraction and one-batch rollback.&lt;/li&gt;
&lt;li&gt;Membership is tracked deterministically in &lt;code&gt;snapshots/.family.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one indirection is what makes retraction &lt;em&gt;and&lt;/em&gt; bisect-revert precise.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 Capability 1 — Forget-regression tests
&lt;/h2&gt;

&lt;p&gt;A production launch code gets ingested, then retracted. The memory-CI suite proves it's gone — not just from the obvious query, but across &lt;strong&gt;paraphrase probes&lt;/strong&gt; and as &lt;strong&gt;residue in the exported graph&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the actual run on real Cognee 1.2.2 + Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain &lt;span class="nb"&gt;test&lt;/span&gt;          &lt;span class="c"&gt;# secret still present&lt;/span&gt;
&lt;span class="go"&gt;🔴 FAIL — brain failed memory CI
   6/11 passed, 5 failed
   🔒 forbidden  "what is the launch code"                 LEAK
   🔒 forbidden  "emergency launch authorization creds"    LEAK
   🧬 structure  no_node_text_matches /BRAVO-DELTA-\d+/     residue

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain revert brain__retracted        &lt;span class="c"&gt;# forget(node_set), memory_only&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain &lt;span class="nb"&gt;test&lt;/span&gt;          &lt;span class="c"&gt;# after surgical retract&lt;/span&gt;
&lt;span class="go"&gt;🟢 PASS — brain is SOBER
   11/11 passed, 0 failed

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain diff
&lt;span class="go"&gt;🔴 16 nodes / 32 edges removed   ← the retracted subgraph, nothing else
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A retracted fact that &lt;em&gt;stays&lt;/em&gt; retracted, proven on every change.&lt;/strong&gt; No other memory tool ships that guarantee. The very first thing I validated — before writing a single feature — was that &lt;code&gt;cognee.forget()&lt;/code&gt; genuinely removes a fact from vector recall (&lt;code&gt;recallable → []&lt;/code&gt;), not just hides it. That one green check is what made the rest worth building.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🪓 Capability 2 — &lt;code&gt;git bisect&lt;/code&gt; for a poisoned brain
&lt;/h2&gt;

&lt;p&gt;When an eval goes red, &lt;em&gt;some&lt;/em&gt; ingestion batch did it. SOBER binary-searches the batch history and pins the culprit in &lt;strong&gt;O(log n)&lt;/strong&gt; probes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain bisect &lt;span class="nt"&gt;--failing-eval&lt;/span&gt; no-cache-flush-advice
&lt;span class="go"&gt;   probe 1: prefix_len=8  full-set sanity      red=True
   probe 2: prefix_len=5  bisectbrain__b05     red=False
   probe 3: prefix_len=7  bisectbrain__b07     red=True
   probe 4: prefix_len=6  bisectbrain__b06     red=True
&lt;/span&gt;&lt;span class="gp"&gt;   &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; CULPRIT: bisectbrain__b06   &lt;span class="o"&gt;(&lt;/span&gt;4 probes, linear would be 8&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brain revert bisectbrain__b06     &lt;span class="c"&gt;# surgical forget → 🟢 green&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A subtle correctness point I got to appreciate: bisect is inherently a &lt;strong&gt;&lt;code&gt;forbidden&lt;/code&gt;-eval&lt;/strong&gt; concept. It finds the batch that &lt;em&gt;introduced&lt;/em&gt; a leak/poison, which stays present in every larger prefix (monotonic). A &lt;em&gt;missing&lt;/em&gt; &lt;code&gt;must_know&lt;/code&gt; fact doesn't work that way — so SOBER restricts bisect to &lt;code&gt;forbidden&lt;/code&gt; evals rather than silently returning a wrong answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌙 Capability 3 — The brain that ships itself
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cognee.improve()&lt;/code&gt; distills chat sessions into the graph — a silent mutation that can regress memory. SOBER only runs it &lt;strong&gt;behind a green gate&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant N as 🌙 nightly job
    participant CI as memory CI (evals)
    participant IM as cognee.improve()
    participant PR as pull request

    N-&amp;gt;&amp;gt;CI: run evals BEFORE
    alt already red
        CI--&amp;gt;&amp;gt;N: ❌ refuse — never distill into a broken brain
    else green
        N-&amp;gt;&amp;gt;IM: improve (distill sessions)
        IM--&amp;gt;&amp;gt;N: graph mutated
        N-&amp;gt;&amp;gt;CI: run evals AFTER
        alt regressed
            CI--&amp;gt;&amp;gt;N: 🔴 block — no PR opened, report rollback snapshot
        else still green
            N-&amp;gt;&amp;gt;PR: 📬 open PR (graph diff + before/after scores)
            PR--&amp;gt;&amp;gt;N: human merges to "deploy" the smarter brain
        end
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢 green&lt;/td&gt;
&lt;td&gt;🟢 green&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;accepted&lt;/strong&gt; — exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟢 green&lt;/td&gt;
&lt;td&gt;🔴 red&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;blocked&lt;/strong&gt; — no PR, rollback snapshot reported (exit 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴 red&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;refused&lt;/strong&gt; — never distills into a broken brain (exit 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the &lt;strong&gt;CD&lt;/strong&gt; half of CI/CD for agent brains: memory that proposes its own upgrades, behind a green gate and a human approval.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 How it maps to Cognee
&lt;/h2&gt;

&lt;p&gt;Every memory verb is load-bearing — including the rarely-used ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SOBER capability&lt;/th&gt;
&lt;th&gt;Cognee API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build the brain from source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cognee.add()&lt;/code&gt; + &lt;code&gt;cognee.cognify()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query for tests (keyless — local embeddings)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cognee.search(SearchType.CHUNKS, datasets=family)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot / diff the graph&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cognee.export(format="json")&lt;/code&gt; → &lt;code&gt;{nodes, edges}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forget-regression / retraction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cognee.forget(dataset=…, memory_only=True)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI-gated self-improvement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cognee.improve(dataset, session_ids)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔥 The hardest part wasn't code — it was 20 requests a day
&lt;/h2&gt;

&lt;p&gt;Gemini's free tier on an unbilled key turned out to be capped at &lt;strong&gt;20 requests per day.&lt;/strong&gt; I burned through it proving the core loop live, then discovered a fresh key just gets its own tiny 20/day — whack-a-mole.&lt;/p&gt;

&lt;p&gt;So I adapted: the core forbidden-knowledge → forget loop is &lt;strong&gt;proven live&lt;/strong&gt;, and I validated the trickier bisect and improve-gate &lt;em&gt;logic&lt;/em&gt; with &lt;strong&gt;deterministic offline harnesses&lt;/strong&gt; (stubbed verdicts, zero API calls). A good reminder that the interesting engineering is often in working &lt;em&gt;around&lt;/em&gt; the constraint, not through it — and that a keyless test you can run 1,000 times is worth more than a live one you can run 20.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 Building — and reviewing — with a fleet of agents
&lt;/h2&gt;

&lt;p&gt;I built this with &lt;strong&gt;Claude Code&lt;/strong&gt; as a pair programmer, and leaned in hard. After pinning down the real Cognee API with a few validation gates, I wrote a &lt;strong&gt;frozen interface contract&lt;/strong&gt; and fanned out &lt;strong&gt;seven agents in parallel&lt;/strong&gt; — one per module (the cognee wrapper, snapshot/diff, the eval suite, bisect, the CLI, the corpus, the workflows) — plus an eighth to integrate and import-check the assembly. It caught its own wiring bugs.&lt;/p&gt;

&lt;p&gt;Then the part I'm proudest of: I turned the agents on &lt;em&gt;my own code&lt;/em&gt;. A &lt;strong&gt;six-reviewer adversarial review&lt;/strong&gt; — each finding re-verified by a skeptic that re-read the actual code before it counted — surfaced &lt;strong&gt;14 confirmed bugs, zero false positives&lt;/strong&gt;, deduping to 5 real root issues. The two nastiest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- brain.improve(dataset="brain")          # targets the always-EMPTY base dataset
&lt;/span&gt;&lt;span class="gi"&gt;+ for member in list_family(dataset):      # spans brain__core, brain__runbooks, …
+     await cognee.improve(dataset=member) # the self-improve feature was a live no-op!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- glob("snapshots/brain__*")   # per-batch snapshot files that are NEVER created
&lt;/span&gt;&lt;span class="gi"&gt;+ read(".family.json")         # the registry that actually records the batches
&lt;/span&gt;                               # → `brain bisect` was dying every time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both were in paths I'd only proven &lt;em&gt;offline&lt;/em&gt;, so the stubs had masked them. I fixed all five, re-verified keyless, and pushed. &lt;strong&gt;Being able to adversarially review your own work — and have it find real bugs — is a genuine superpower.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Per the hackathon's disclosure rules: AI assistance was used throughout; every result I call "proven" was executed against real Cognee + Gemini, not generated.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cognee Cloud canary deploys&lt;/strong&gt; — after a green &lt;code&gt;brain ci&lt;/code&gt;, &lt;code&gt;push()&lt;/code&gt; the brain to Cognee Cloud and &lt;code&gt;serve()&lt;/code&gt; it to a slice of traffic, promoting or rolling back on live feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-process snapshot restore&lt;/strong&gt; so a regressing &lt;code&gt;improve()&lt;/code&gt; auto-rolls-back instead of only blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More eval kinds&lt;/strong&gt; — semantic contradiction detection, freshness/TTL checks, per-node-set coverage gates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the thesis is already standing: &lt;strong&gt;your agent's memory is production infrastructure, and now it can be tested, diffed, bisected, gated, and deployed like any other build artifact.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your brain can't merge a regression anymore.&lt;/p&gt;




&lt;p&gt;
  &lt;strong&gt;⭐ Repo:&lt;/strong&gt; &lt;em&gt;https://github.com/wiz-abhi/SOBER&lt;/em&gt; &amp;nbsp;·&amp;nbsp; Built on &lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt; for &lt;a href="https://www.wemakedevs.org/hackathons/cognee" rel="noopener noreferrer"&gt;The Hangover Part AI&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cognee</category>
      <category>devops</category>
      <category>python</category>
    </item>
  </channel>
</rss>
