<?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: Anthahkarana</title>
    <description>The latest articles on DEV Community by Anthahkarana (@anthah).</description>
    <link>https://dev.to/anthah</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%2F3303541%2F4bc52a88-d2bf-4d5f-b278-621a79c5ee0a.jpg</url>
      <title>DEV Community: Anthahkarana</title>
      <link>https://dev.to/anthah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anthah"/>
    <language>en</language>
    <item>
      <title>My AI agent didn't crash. It developed OCD.</title>
      <dc:creator>Anthahkarana</dc:creator>
      <pubDate>Sun, 26 Jul 2026 22:01:23 +0000</pubDate>
      <link>https://dev.to/anthah/my-ai-agent-didnt-crash-it-developed-ocd-5c5a</link>
      <guid>https://dev.to/anthah/my-ai-agent-didnt-crash-it-developed-ocd-5c5a</guid>
      <description>&lt;p&gt;Let me be honest about where this started: I did not know much about observability. Spans, traces, exporters, OTLP endpoints, these were words other people used at work. I build agents, and when mine broke, my debugging strategy was reading logs and guessing.&lt;/p&gt;

&lt;p&gt;Then the Agents of SigNoz hackathon showed up, and I signed up anyway, on the theory that a deadline is the fastest teacher. This is the story of going from "what exactly is a span?" to shipping a working psychiatric hospital for AI agents in a couple of weeks, paired with &lt;strong&gt;Claude Code&lt;/strong&gt; the whole way. I'll be honest about which parts were me and which were the machine, because that split was the most interesting part of the project.&lt;/p&gt;

&lt;p&gt;It began with a trace I couldn't stop staring at. Latency normal, error rate zero, every span green. The agent had called &lt;code&gt;web_search("confirm pincode 110011")&lt;/code&gt; eighteen times with identical arguments, produced nothing, and by every dashboard I had, it was healthy.&lt;/p&gt;

&lt;p&gt;Agents rarely fail the way services fail. They don't 500. They go insane: quietly, expensively, with a perfectly green dashboard. So I built them a hospital: &lt;strong&gt;AIIMS&lt;/strong&gt;, the All India Institute of Machine Sanity. Sick agents get admitted, diagnosed, and treated. The joke was my starting point; making it sit on real, detectable failures took everything I had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting a trap for my own agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5zlxkuoptz77pafm6oi.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%2Fr5zlxkuoptz77pafm6oi.png" alt="ward-board" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first week was remedial: reading OpenTelemetry docs, asking Claude beginner questions I was too embarrassed to ask an SRE. The decision I'm glad I made early: instead of instrumenting one real app and hoping it misbehaved, I built five small tool-using agents, each engineered to fail one way, and shipped their traces to SigNoz with GenAI semantic conventions on the attributes.&lt;/p&gt;

&lt;p&gt;The five failure modes, all of which I've hit for real in agent code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looping on an identical tool call until something kills the process&lt;/li&gt;
&lt;li&gt;Retrying a dead endpoint in a tightening storm&lt;/li&gt;
&lt;li&gt;Inflating context until the model forgets the task and answers a different question&lt;/li&gt;
&lt;li&gt;Stalling mid-run, holding a lock, alive and doing nothing&lt;/li&gt;
&lt;li&gt;Crashing outright, the only one a normal dashboard actually catches&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ordinary panels missed cases 1 to 4: p95 latency fine, error rate near zero. Case 3 is nastiest, the run &lt;em&gt;succeeds&lt;/em&gt;, returns a confident answer to a question nobody asked. Green dashboard, broken product. The signal was in the traces the whole time; eighteen spans sharing a tool name and an argument hash isn't subtle once you look, but looking required already suspecting it. So instead of spotting these by eye, I wrote detectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a detector that reads traces, not metrics
&lt;/h2&gt;

&lt;p&gt;Here the pairing rhythm settled in: I'd describe a pathology, Claude would draft the detector, and I'd poke holes in it, usually by feeding it a healthy trace and watching it panic. Each pathology is one pure function over a list of span dicts, testable without SigNoz, an agent, or an LLM in the loop.&lt;/p&gt;

&lt;p&gt;The loop detector is the simplest and caught the most real bugs:&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;detect_chakravyuh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The same tool called with the same arguments, over and over.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_tool_spans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributes&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;gen_ai.tool.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
               &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&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.tool.args_hash&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="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repeats&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;REPEAT_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The load-bearing detail is &lt;code&gt;args_hash&lt;/code&gt;, a stable hash of the tool arguments emitted as a span attribute at call time:&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;args_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sort_keys=True&lt;/code&gt; is not optional. Without it, &lt;code&gt;{"a":1,"b":2}&lt;/code&gt; and &lt;code&gt;{"b":2,"a":1}&lt;/code&gt; hash differently and the loop becomes invisible. I lost twenty minutes to that: the bug wasn't in anything clever, just a default argument I didn't know existed. Hashing instead of storing raw arguments also solved a real problem: arguments can carry user data you don't want in telemetry, and a 12-character hash groups perfectly in SigNoz while leaking nothing.&lt;/p&gt;

&lt;p&gt;The context-drift detector was the one I expected to need embeddings for. A Jaccard overlap of content words between the run's stated goal and the current task, combined with context-window pressure, catches the real cases. Neither signal alone is enough: high pressure while still on-task is just a long run, low overlap early on is just a subtask. It's the &lt;strong&gt;conjunction&lt;/strong&gt;, "the window filled up and the goal fell out of it," that was the single most useful modelling insight in the project. Every detector ships with two tests, one proving it fires and one proving it stays quiet on healthy runs. A diagnostic panel that diagnoses everyone is not a diagnostic panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feeding the diagnosis back into SigNoz
&lt;/h2&gt;

&lt;p&gt;The detectors produce a 0 to 100 sanity score per run. My first instinct was to show it in my own UI, the wrong instinct. Pushing it into SigNoz as a real metric was the best architectural call in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sanity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_gauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aiims.patient.sanity_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sanity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&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;agent.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it sits next to the traces it was derived from, I get dashboard panels, correlation with raw spans, and, the actual point, &lt;strong&gt;alerting&lt;/strong&gt;: a threshold rule on &lt;code&gt;sanity_score &amp;lt; 35&lt;/code&gt; pages me when an agent is losing its mind, which no latency alert ever would. One gotcha that cost me an evening: the exporter needs the signal path appended to the SigNoz endpoint (&lt;code&gt;/v1/traces&lt;/code&gt;), and the failure mode is silence, not an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating the patient
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1cjqeevoibfjlycyt2b.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%2Fi1cjqeevoibfjlycyt2b.png" alt="clinical-trial" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the alert fired reliably, the obvious question was: why am I in this loop at all? By the time I read it, the money is already spent. So the same detectors now run in-process, during the run. When a disorder crosses a threshold, the supervisor changes what the agent can do next: refuses the repeated call, trips a circuit breaker on a dead tool, enforces a token budget, re-injects the original goal. Every intervention is itself emitted as a span, so I can prove a loop was broken instead of just claiming it.&lt;/p&gt;

&lt;p&gt;Then I ran each agent twice, changing only whether the supervisor was attached: same agents, same scripted pathologies, same goals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  patient      disorder   arm         steps     tokens  outcome
  abhimanyu    loop       untreated      20    101,954  survived
                          treated        11     22,208  survived
  damini       stall      untreated       2      2,060  DIED
                          treated         4      4,580  survived

  TOTAL   tokens 1,497,222 → 582,499 (61% saved), deaths 1 → 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note damini spends &lt;em&gt;more&lt;/em&gt; tokens under treatment, which is correct, since she finishes the job instead of dying halfway. I nearly "fixed" that row before realising completed runs, not spend, was the metric I cared about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fng6fgoyheczjlu8nojpg.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%2Fng6fgoyheczjlu8nojpg.png" alt="morgue" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A rule I kept from day one: nothing Claude produced was accepted on its word, every finding got checked against live SigNoz first. That's the only reason these are a blog section instead of surprises in front of judges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A therapeutic abort is not a crash.&lt;/strong&gt; When the budget guard killed a runaway run, my own death detector filed a death certificate for it, so the system punished itself for working. The fix was an attribute the detector skips on deliberate stops. Stops and failures look identical in span status; only you know the difference, so you have to record it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One global tracer provider will silently lie to you.&lt;/strong&gt; Running several agents in one process, I called &lt;code&gt;set_tracer_provider()&lt;/code&gt; per agent. OpenTelemetry logs a warning, not an error, and quietly keeps the first provider. Every agent after the first had an empty span buffer, so every diagnosis came back "perfectly healthy." My comparison table was garbage for an hour. Thresholds are the whole game beyond that: my first loop detector fired at 2 repeats and flagged every agent that retried anything, until I tuned it against agents deliberately broken one specific way each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually built this
&lt;/h2&gt;

&lt;p&gt;"Built with AI" can mean "I typed one prompt" or "I did everything and mentioned AI for the algorithm." Neither is true here. The idea and framing were mine, the hospital, the disorder taxonomy, the conviction that a diagnosis which doesn't change an outcome is decoration, so the ward had to treat, not just report, and every architecture fork was my call. Claude Code wrote most of the actual lines: detectors, supervisor, ward service, tests, UI. I directed, reviewed, rejected, verified. Three weeks ago that split would have felt like cheating; now it's just what building looks like. Taste and accountability stayed human, the typing largely didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Agent failures are behavioural, not statistical: the spans are all successful and the shape between them is what's wrong. Traces already contain that shape. Write detectors over span data, push the result back as a metric so it can page you, and once it can page you, ask why a human is in the loop at all.&lt;/p&gt;

&lt;p&gt;If you're staring at a field you know nothing about: I could not define "span" when this started. Pick a deadline, bring a pair partner that never tires of your questions, and refuse to accept anything you haven't verified yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code, detectors and dashboards:&lt;/strong&gt; &lt;a href="https://github.com/githubber-me/aiims" rel="noopener noreferrer"&gt;github.com/githubber-me/aiims&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz&lt;/a&gt; hackathon, following the &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
