<?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: Nagarjuna Bolla</title>
    <description>The latest articles on DEV Community by Nagarjuna Bolla (@nagarjuna325).</description>
    <link>https://dev.to/nagarjuna325</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%2F4048287%2Fc09abd5e-488d-4157-a382-c57c5ab47c58.png</url>
      <title>DEV Community: Nagarjuna Bolla</title>
      <link>https://dev.to/nagarjuna325</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nagarjuna325"/>
    <language>en</language>
    <item>
      <title>I Built Four AI Agents That Fix Bugs by Themselves, Then Made Them Watch Themselves Work</title>
      <dc:creator>Nagarjuna Bolla</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:32:32 +0000</pubDate>
      <link>https://dev.to/nagarjuna325/-i-built-four-ai-agents-that-fix-bugs-by-themselves-then-made-them-watch-themselves-work-2dk2</link>
      <guid>https://dev.to/nagarjuna325/-i-built-four-ai-agents-that-fix-bugs-by-themselves-then-made-them-watch-themselves-work-2dk2</guid>
      <description>&lt;p&gt;We've gotten good at observing software. We're not yet good at observing the &lt;em&gt;agents&lt;/em&gt;&lt;br&gt;
that increasingly write and operate that software.&lt;/p&gt;

&lt;p&gt;When a service throws a 500, you open a trace and see exactly which span failed. When&lt;br&gt;
an AI agent does something wrong, you usually get… a log line saying it failed. Maybe&lt;br&gt;
a stack trace. Rarely the reasoning, the tool calls it made, what it cost, or — most&lt;br&gt;
importantly — &lt;em&gt;whether the failure was the agent's fault at all.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So for the Agents of SigNoz hackathon I built &lt;strong&gt;AgentOps&lt;/strong&gt;: four autonomous agents&lt;br&gt;
that detect, diagnose, fix, and verify a real bug in a real application with no human&lt;br&gt;
in the loop. The self-healing part is the demo. The actual project is that &lt;strong&gt;every&lt;br&gt;
thought those agents have is a span in SigNoz.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I deliberately did not scaffold my own toy app to break. Fixing a bug you planted in&lt;br&gt;
code you wrote is not convincing.&lt;/p&gt;

&lt;p&gt;Instead I vendored &lt;strong&gt;&lt;a href="https://github.com/AutomationPanda/buggyboard-web-app" rel="noopener noreferrer"&gt;BuggyBoard&lt;/a&gt;&lt;/strong&gt;,&lt;br&gt;
an MIT-licensed bug tracker by Andrew Knight — Express, TypeScript, SQLite. Foreign&lt;br&gt;
code, with a commit history that predates this project. Then I seeded exactly one&lt;br&gt;
defect into its not-found guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  const bug = getBug(id);
&lt;span class="gd"&gt;- if (!bug) {
&lt;/span&gt;&lt;span class="gi"&gt;+ if (!bug.id) {
&lt;/span&gt;    res.status(404).json({ error: "not_found", message: "Bug not found." });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;GET /api/bugs/999999&lt;/code&gt; dereferences &lt;code&gt;null&lt;/code&gt;, throws a &lt;code&gt;TypeError&lt;/code&gt;, and Express&lt;br&gt;
returns &lt;strong&gt;500&lt;/strong&gt; where it should return &lt;strong&gt;404&lt;/strong&gt;. A boring, extremely realistic bug.&lt;/p&gt;

&lt;p&gt;There's a nice symmetry to it: the agents fix a bug in a bug tracker.&lt;/p&gt;


&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monitor  →  Diagnosis  →  Fix  →  Verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Monitor&lt;/strong&gt; polls SigNoz every four seconds asking "any error spans from &lt;code&gt;demo-api&lt;/code&gt;&lt;br&gt;
since my last check?" It has &lt;strong&gt;no privileged backchannel&lt;/strong&gt; to the application. It&lt;br&gt;
finds out the same way a human on-call engineer would — by querying the observability&lt;br&gt;
backend. It makes zero LLM calls; the threshold is deterministic. Its agenthood is the&lt;br&gt;
autonomous loop, not cleverness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis&lt;/strong&gt; pulls the ERROR logs and the full trace from SigNoz, hands &lt;em&gt;only that&lt;br&gt;
evidence&lt;/em&gt; to an LLM, and demands strict JSON back: file, line, cause. It answers&lt;br&gt;
&lt;code&gt;src/index.ts:87&lt;/code&gt;. It never reads the source code, and it certainly never reads the&lt;br&gt;
answer key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; reads the suspect file through a sandboxed filesystem MCP server, makes one&lt;br&gt;
LLM call for a minimal patch, validates it locally, writes it, and restarts the&lt;br&gt;
service — rolling back to the original bytes if the patched service won't come up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify&lt;/strong&gt; replays the exact failing request and demands exactly a &lt;code&gt;404&lt;/code&gt;, then&lt;br&gt;
re-queries SigNoz to confirm no new errors.&lt;/p&gt;

&lt;p&gt;Total: &lt;strong&gt;30 to 60 seconds&lt;/strong&gt;, unattended.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📸 &lt;em&gt;Screenshot: mission-control with the four agents mid-pipeline&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The part that actually matters: the agents query SigNoz as a tool
&lt;/h2&gt;

&lt;p&gt;This is the design decision I'd defend hardest.&lt;/p&gt;

&lt;p&gt;SigNoz isn't where these agents &lt;em&gt;report&lt;/em&gt; — it's where they &lt;em&gt;perceive&lt;/em&gt;. Through the&lt;br&gt;
&lt;strong&gt;SigNoz MCP server&lt;/strong&gt; (which Foundry installs alongside SigNoz in one step), the&lt;br&gt;
agents call real tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signoz_search_traces&lt;/code&gt; — Monitor's detection loop, and Verify's post-fix check&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signoz_search_logs&lt;/code&gt; — Diagnosis's primary evidence, because the stack trace lives
in the log body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signoz_get_trace_details&lt;/code&gt; — the full span tree for the failing request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because I wrapped every one of those calls in a span, &lt;strong&gt;the agents' use of SigNoz&lt;br&gt;
is itself observable in SigNoz.&lt;/strong&gt; You can see how long each tool call took and whether&lt;br&gt;
it succeeded.&lt;/p&gt;

&lt;p&gt;That recursion is the whole thesis of the project.&lt;/p&gt;


&lt;h2&gt;
  
  
  One incident = one trace
&lt;/h2&gt;

&lt;p&gt;Two OTel services exist: &lt;code&gt;demo-api&lt;/code&gt; (the app) and &lt;code&gt;sdlc-agents&lt;/code&gt; (the swarm). A single&lt;br&gt;
incident produces one continuous trace of about 14 spans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent.orchestrator.incident              ← root: incident.id, phase events, span link
├── agent.diagnosis.run
│   ├── gen_ai.tool.signoz_search_logs
│   ├── gen_ai.tool.signoz_get_trace_details
│   └── agent.diagnosis.llm              ← model + input/output tokens
├── agent.fix.run
│   ├── gen_ai.tool.read_text_file
│   ├── agent.fix.llm
│   ├── gen_ai.tool.write_file
│   └── agent.fix.restart
└── agent.verify.run
    ├── agent.verify.replay              ← http.request.method, status 404
    └── gen_ai.tool.signoz_search_traces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📸 &lt;em&gt;Screenshot: the SigNoz trace waterfall for one incident&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few conventions I locked down early and never broke:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent steps are &lt;code&gt;agent.&amp;lt;name&amp;gt;.&amp;lt;step&amp;gt;&lt;/code&gt;. MCP tool calls are &lt;code&gt;gen_ai.tool.&amp;lt;literal tool&lt;br&gt;
name&amp;gt;&lt;/code&gt;, always as a child of the step that invoked them.&lt;/strong&gt; The &lt;code&gt;gen_ai.tool.*&lt;/code&gt;&lt;br&gt;
namespace is strictly for MCP calls. When the Fix agent restarts a process or the&lt;br&gt;
Verify agent makes an HTTP request, those are &lt;em&gt;steps&lt;/em&gt; — &lt;code&gt;agent.fix.restart&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;agent.verify.replay&lt;/code&gt; — never dressed up as tool calls. Keeping that boundary clean is&lt;br&gt;
what makes the trace readable at a glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every LLM call carries &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;,&lt;/strong&gt; per the OTel GenAI semantic conventions. Token spend&lt;br&gt;
per agent falls straight out of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;incident.id&lt;/code&gt; is propagated through OTel Context, not function parameters.&lt;/strong&gt; This one&lt;br&gt;
paid off. Threading an ID through every agent signature would have touched every&lt;br&gt;
function and silently missed any span whose caller forgot to pass it. Instead one&lt;br&gt;
Context key, read inside the two span-opening wrappers, stamps all 14 spans —&lt;br&gt;
including code I never modified. If you've used a &lt;code&gt;ThreadLocal&lt;/code&gt; for a request ID in&lt;br&gt;
Java, it's exactly that, except AsyncLocalStorage keeps it correct across &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root span carries a span &lt;em&gt;link&lt;/em&gt; to the Monitor poll that detected the&lt;br&gt;
incident&lt;/strong&gt; — "caused by, not parented by." The poll is its own root trace; it existed&lt;br&gt;
before the incident did, so parenting would be a lie about causality.&lt;/p&gt;


&lt;h2&gt;
  
  
  Failure honesty, and why it's the best thing I built
&lt;/h2&gt;

&lt;p&gt;Early on, a failed run just said "failed." Useless. I couldn't tell a bad hypothesis&lt;br&gt;
from an API outage.&lt;/p&gt;

&lt;p&gt;So failures became explicit, on the span:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llm_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model failed or returned junk, or a pre-write guard rejected the patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wrong_hypothesis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process is up but still misbehaving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patch_broke_process&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Patch wouldn't boot; rollback succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rollback_failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Even rollback wouldn't boot — stop, alert a human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;telemetry_unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz/MCP is down — infrastructure, not reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fs_mcp_unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The filesystem MCP transport died&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule that makes it work: &lt;strong&gt;a dead telemetry pipeline must never be reported as an&lt;br&gt;
agent reasoning badly.&lt;/strong&gt; Those are completely different problems with completely&lt;br&gt;
different fixes, and collapsing them into "failed" destroys the only signal that&lt;br&gt;
matters.&lt;/p&gt;

&lt;p&gt;There are also &lt;strong&gt;no retries anywhere&lt;/strong&gt;. One attempt. If it fails, it fails loudly in&lt;br&gt;
the trace. Retries would have hidden exactly the failures I most wanted to see.&lt;/p&gt;

&lt;p&gt;This paid for itself twice in one afternoon. Two runs failed; the spans told me&lt;br&gt;
instantly that one was a Gemini &lt;code&gt;503&lt;/code&gt; (transient, provider-side) and the other was a&lt;br&gt;
telemetry ingest race (mine). Same symptom, completely different causes, zero&lt;br&gt;
debugging time.&lt;/p&gt;


&lt;h2&gt;
  
  
  SigNoz caught a bug in my own agent
&lt;/h2&gt;

&lt;p&gt;My favourite moment.&lt;/p&gt;

&lt;p&gt;The Verify agent waited a flat 5 seconds after the restart, then queried SigNoz once&lt;br&gt;
to confirm no new errors. A run failed with &lt;code&gt;telemetry_unavailable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent was right and my tuning was wrong. Spans take &lt;strong&gt;5 to 10 seconds&lt;/strong&gt; to become&lt;br&gt;
queryable after export. Verify was racing ingest, finding an empty window, and — &lt;br&gt;
correctly, by its own logic — reporting that the pipeline looked dead.&lt;/p&gt;

&lt;p&gt;The fix was to separate &lt;em&gt;waiting for data to arrive&lt;/em&gt; from &lt;em&gt;asserting what the data&lt;br&gt;
says&lt;/em&gt;: poll the same fixed window every 3 seconds up to 21 seconds until it's&lt;br&gt;
non-empty, then evaluate the assertion exactly once. It's the difference between&lt;br&gt;
&lt;code&gt;Thread.sleep(5000)&lt;/code&gt; before an assert and &lt;code&gt;await().atMost(21, SECONDS).until(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I only found it because the failure was visible in the trace instead of swallowed by&lt;br&gt;
a retry.&lt;/p&gt;


&lt;h2&gt;
  
  
  Dashboards, and a lesson about metric labels
&lt;/h2&gt;

&lt;p&gt;Two dashboards, version-controlled as JSON and provisioned idempotently through the&lt;br&gt;
SigNoz API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Fleet Health&lt;/strong&gt; — per-agent invocations, p50/p95 latency, LLM token spend by&lt;br&gt;
agent, MCP tool-call breakdown, failure rate, and failures by reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo API Health&lt;/strong&gt; — request volume, 5xx count, and a per-status-code breakdown of&lt;br&gt;
the failing route that visibly goes red at trigger and green after the fix.&lt;/p&gt;

&lt;p&gt;Plus a threshold alert on agent failure rate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📸 &lt;em&gt;Screenshot: the Agent Fleet Health dashboard&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The non-obvious part was &lt;strong&gt;which data source each panel needs.&lt;/strong&gt; SigNoz's span metrics&lt;br&gt;
do cover internal spans — the span name shows up as an &lt;code&gt;operation&lt;/code&gt; label, which I&lt;br&gt;
didn't expect. But they carry only &lt;code&gt;service.name&lt;/code&gt;, &lt;code&gt;operation&lt;/code&gt;, &lt;code&gt;span.kind&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;status.code&lt;/code&gt;. Every custom attribute I'd carefully added — token counts,&lt;br&gt;
&lt;code&gt;gen_ai.agent.name&lt;/code&gt;, &lt;code&gt;incident.id&lt;/code&gt;, failure reason, even &lt;code&gt;http.route&lt;/code&gt; — exists &lt;strong&gt;only&lt;br&gt;
on spans&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So panels are split per-panel: latency and invocation counts come from metrics&lt;br&gt;
(cheap, pre-aggregated), while anything needing a custom dimension queries traces.&lt;/p&gt;

&lt;p&gt;The general lesson: &lt;em&gt;pre-aggregated metrics only carry the dimensions someone declared&lt;br&gt;
in advance.&lt;/em&gt; Assuming your custom span attributes are queryable as metric labels gets&lt;br&gt;
you panels that render empty rather than erroring — which is worse, because empty&lt;br&gt;
looks like "nothing happened."&lt;/p&gt;

&lt;p&gt;Two more traps worth writing down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signoz_calls_total&lt;/code&gt; is a &lt;strong&gt;monotonic delta counter&lt;/strong&gt;, so it needs
&lt;code&gt;timeAggregation: "increase"&lt;/code&gt;. The default &lt;code&gt;rate&lt;/code&gt; silently returns nothing against
sparse demo traffic.&lt;/li&gt;
&lt;li&gt;Metric labels are &lt;strong&gt;dotted&lt;/strong&gt; (&lt;code&gt;service.name&lt;/code&gt;). The underscore form doesn't error —
it returns empty group keys with a non-zero aggregate. Silently wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because "the API returned 200" proves nothing, every panel is validated by a&lt;br&gt;
committed script that runs &lt;strong&gt;the panel's own query&lt;/strong&gt; and fails any panel returning&lt;br&gt;
empty or all-zero data. I run it over a narrow 15-minute window containing only a&lt;br&gt;
fresh incident, so the panels have to populate from live data rather than accumulated&lt;br&gt;
history. 16/16.&lt;/p&gt;


&lt;h2&gt;
  
  
  Does it generalise, or did I just special-case my own bug?
&lt;/h2&gt;

&lt;p&gt;The obvious challenge. So I tested it.&lt;/p&gt;

&lt;p&gt;I injected a &lt;strong&gt;completely different&lt;/strong&gt; defect the system had never seen — a different&lt;br&gt;
route (&lt;code&gt;GET /api/bugs&lt;/code&gt;, the list endpoint), a different error class (calling a method&lt;br&gt;
that doesn't exist rather than dereferencing null), at a different line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  const bugs = listBugs();
&lt;span class="gd"&gt;- res.json(bugs);
&lt;/span&gt;&lt;span class="gi"&gt;+ res.json(bugs.sortByPriority());
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No code changes. No prompt changes. I just broke it and watched.&lt;/p&gt;

&lt;p&gt;Monitor detected it. Diagnosis reported &lt;strong&gt;&lt;code&gt;src/index.ts:77&lt;/code&gt;&lt;/strong&gt; with the cause&lt;br&gt;
&lt;em&gt;"attempts to invoke &lt;code&gt;bugs.sortByPriority()&lt;/code&gt;, but &lt;code&gt;sortByPriority&lt;/code&gt; is not a defined&lt;br&gt;
function on &lt;code&gt;bugs&lt;/code&gt;"&lt;/em&gt; — exactly right. Fix repaired it. Verify confirmed it. Incident&lt;br&gt;
closed &lt;strong&gt;resolved&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing about that bug was anticipated anywhere in the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Guardrails
&lt;/h2&gt;

&lt;p&gt;An agent that rewrites source and restarts a live service needs hard limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least privilege.&lt;/strong&gt; The Fix agent reaches the codebase only through a filesystem MCP&lt;br&gt;
server rooted at the backend directory. It cannot resolve a path outside it — a&lt;br&gt;
committed test asserts that relative traversal, absolute paths, directory listing, and&lt;br&gt;
search are all refused.&lt;/p&gt;

&lt;p&gt;One detail I nearly got wrong: the filesystem MCP server &lt;em&gt;replaces its own allow-list&lt;br&gt;
with client-supplied roots&lt;/em&gt; if the client advertises the &lt;code&gt;roots&lt;/code&gt; capability. My client&lt;br&gt;
declares no capabilities, deliberately. A server-side restriction is only as strong as&lt;br&gt;
the negotiation that can rewrite it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-write validation.&lt;/strong&gt; A patch must match exactly once, must actually change&lt;br&gt;
something, and is capped at 300 characters per side and a 200-byte net delta.&lt;br&gt;
Violations abort before any write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic rollback.&lt;/strong&gt; Original bytes captured before writing; restored if the patched&lt;br&gt;
service fails its health check. Mechanical reversion, not a second guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things that cost me time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A model in ListModels is not a model you can call.&lt;/strong&gt; &lt;code&gt;gemini-2.5-flash&lt;/code&gt; appeared in&lt;br&gt;
the listing and returned 404 "no longer available to new users" on the actual call.&lt;br&gt;
A listing endpoint is not an entitlement check — verify against &lt;em&gt;your&lt;/em&gt; credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free-tier quota is per model, per day.&lt;/strong&gt; 20 requests/day/model, and each incident&lt;br&gt;
spends 2. A day of rehearsals exhausted one model. Because the quota is per-model,&lt;br&gt;
switching IDs buys another 20 — so the model became an environment variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini flash models think by default, and thinking tokens come out of&lt;br&gt;
&lt;code&gt;maxOutputTokens&lt;/code&gt;.&lt;/strong&gt; With a 1024-token budget, the entire allowance was consumed&lt;br&gt;
thinking before any JSON was emitted — &lt;code&gt;finishReason: MAX_TOKENS&lt;/code&gt;, empty text. An&lt;br&gt;
empty response at MAX_TOKENS is a budget symptom, not a prompt-quality problem. Don't&lt;br&gt;
start by rewriting the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication succeeding is not authorization working.&lt;/strong&gt; A fresh SigNoz&lt;br&gt;
service-account key returned 200 on &lt;code&gt;/api/v1/version&lt;/code&gt; and on the identity endpoint&lt;br&gt;
while every data route returned 403 — because the account had no role attached. Test a&lt;br&gt;
&lt;em&gt;privileged&lt;/em&gt; route, not a version route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrumenting an ESM app needs more than &lt;code&gt;--require&lt;/code&gt;.&lt;/strong&gt; Node's ESM→CJS path bypasses&lt;br&gt;
require-in-the-middle, so a preload alone patched &lt;code&gt;node:http&lt;/code&gt; but silently left Express&lt;br&gt;
unpatched. Spans still appeared — just named bare &lt;code&gt;GET&lt;/code&gt; with no route. When verifying&lt;br&gt;
OTel setup, check span &lt;em&gt;names and route attributes&lt;/em&gt;, not just span presence.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Detection is error-shaped.&lt;/strong&gt; Monitor triggers on 5xx and exception spans. A silent&lt;br&gt;
logic bug returning HTTP 200 with a wrong answer would never be noticed. Catching that&lt;br&gt;
needs assertions about correctness, not just errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify's replay is bug-specific.&lt;/strong&gt; It re-requests the known failing URL. For the&lt;br&gt;
novel bug, the general safety net was the "zero error spans after restart" check. A&lt;br&gt;
more general Verify would derive its replay from the incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The self-healing loop is a good demo. But the thing I'd actually want in production is&lt;br&gt;
the boring part: &lt;strong&gt;when an agent fails, I want to know whether it was the agent's&lt;br&gt;
fault&lt;/strong&gt;, and I want to answer that from a trace rather than from a hunch.&lt;/p&gt;

&lt;p&gt;That means treating agents like services — spans for reasoning steps, child spans for&lt;br&gt;
tool calls, token usage as a first-class metric, and failure reasons specific enough&lt;br&gt;
to act on. SigNoz's MCP server made the interesting half possible, because the agents&lt;br&gt;
could consume observability rather than just emit it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Nagarjuna325/Agent-Observability" rel="noopener noreferrer"&gt;https://github.com/Nagarjuna325/Agent-Observability&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: TypeScript, OpenTelemetry, SigNoz OSS (deployed via Foundry), SigNoz MCP&lt;br&gt;
server, Google Gemini, Express, React. Demo app: BuggyBoard by Andrew Knight (MIT).&lt;/em&gt;&lt;/p&gt;

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