<?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: Bereket Legesse</title>
    <description>The latest articles on DEV Community by Bereket Legesse (@bek-dev-22).</description>
    <link>https://dev.to/bek-dev-22</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%2F4048204%2F982b6fb0-5cbe-4174-a8d6-971610554830.png</url>
      <title>DEV Community: Bereket Legesse</title>
      <link>https://dev.to/bek-dev-22</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bek-dev-22"/>
    <language>en</language>
    <item>
      <title>Taraol: making a distributed AI-agent mesh observable and self-defending — with SigNoz</title>
      <dc:creator>Bereket Legesse</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:03:35 +0000</pubDate>
      <link>https://dev.to/bek-dev-22/how-we-caught-and-contained-a-runaway-ai-agent-loop-with-signoz-1548</link>
      <guid>https://dev.to/bek-dev-22/how-we-caught-and-contained-a-runaway-ai-agent-loop-with-signoz-1548</guid>
      <description>&lt;p&gt;Have you ever shipped a multi-agent app, watched every request come back &lt;code&gt;200&lt;/code&gt;, and still had no real idea what your agents were doing to &lt;em&gt;each other&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Taraol is an SDK&lt;/strong&gt; (one &lt;code&gt;pip install&lt;/code&gt;) that answers exactly that. Drop two decorators on your Python agents and you get full OpenTelemetry tracing, per-call token and cost tracking, a&lt;br&gt;
live topology, conversation replay, operational experiments, and automatic loop/injection defense. &lt;/p&gt;

&lt;p&gt;It's built entirely on &lt;strong&gt;OpenTelemetry&lt;/strong&gt; and &lt;strong&gt;SigNoz&lt;/strong&gt;: no proprietary SDK, no custom UI, and it works with any Python agent (Gemini, OpenAI, Anthropic, LangChain, or your own code).&lt;/p&gt;

&lt;p&gt;What's in the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Distributed tracing&lt;/strong&gt;: gen_ai semantic conventions, one trace id across processes&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Token + cost per call&lt;/strong&gt;: read straight off the SDK response&lt;/li&gt;
&lt;li&gt;🗺️ &lt;strong&gt;Live Service Map&lt;/strong&gt;: your agent topology, drawn by SigNoz from traces&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;AgentLab&lt;/strong&gt;: compare agent &lt;em&gt;designs&lt;/em&gt; on real operational telemetry&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;Self-defense&lt;/strong&gt;: runaway-loop detection, self-healing circuit breakers, injection taint&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Closed loop&lt;/strong&gt;: detection → SigNoz alert → automatic enforcement → audit&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Picture five agents
&lt;/h3&gt;

&lt;p&gt;Say you run &lt;code&gt;planner → researcher → writer → critic → router&lt;/code&gt;, each in its own service, talking over HTTP. From the outside, everything's green. But the writer and critic get stuck revising each other forever, and &lt;em&gt;no single service can see it&lt;/em&gt;, because the loop only exists in the traffic &lt;strong&gt;between&lt;/strong&gt; them.&lt;/p&gt;

&lt;p&gt;Here's what Taraol gives you for that system, in four verbs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observe:&lt;/strong&gt; every agent, tool, and model call is a span in SigNoz, with tokens and cost, stitched into one distributed trace. The five services render as a Service Map on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug:&lt;/strong&gt; replay the whole conversation from telemetry: each agent's input → output → tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defend:&lt;/strong&gt; a watcher reads the telemetry, spots the writer↔critic loop no agent could see, and a per-edge circuit breaker cuts it, then heals the edge automatically once it recovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve:&lt;/strong&gt; run the converging design and the runaway design as one experiment and compare them on cost, latency, loops, and breaker trips. AgentLab tells you which to ship.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the proof, from that exact demo, same workload, two designs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;variant       cost$   tokens  agents   avg ms  loops  breakers  fails  health
baseline     0.0116     4127       5    42460      0         0      0    91.5
runaway      0.0266     9457       5   130972      1         1      0    58.8

Highest Operational Health: baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two prompts. Both produce a fine answer. One costs 2.3× more, runs 3× longer, spins a runaway loop, and trips a circuit breaker on the way. &lt;strong&gt;No prompt-eval tool will ever tell you that&lt;/strong&gt;: it's not a quality difference, it's an operational one, and it only exists in telemetry.&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%2Fbp8swx4bsabo9npz4pf5.gif" 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%2Fbp8swx4bsabo9npz4pf5.gif" alt="The same workload, two designs"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The failure modes are structural, not accidental
&lt;/h2&gt;

&lt;p&gt;Modern AI apps aren't a prompt anymore. They're teams. And teams fail in ways a single service doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two agents &lt;strong&gt;revise each other forever&lt;/strong&gt;. Every iteration is individually valid, every call returns 200, and every loop is a real LLM bill.&lt;/li&gt;
&lt;li&gt;A poisoned document &lt;strong&gt;injects instructions&lt;/strong&gt; that spread silently to every downstream agent that reads the output.&lt;/li&gt;
&lt;li&gt;In a distributed system, &lt;strong&gt;no single agent can see either problem.&lt;/strong&gt; The planner has no idea the writer and critic are ping-ponging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the load-bearing one. The pathology doesn't exist inside any one process; it exists in the &lt;em&gt;shape of the traffic between them&lt;/em&gt;. So the only place you can detect it is the telemetry.&lt;/p&gt;

&lt;p&gt;Which is convenient, because if detection reads telemetry rather than application code, it works regardless of framework, language, or who wrote which agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Observe: three lines, and you keep your code
&lt;/h2&gt;

&lt;p&gt;The integration is deliberately boring. &lt;code&gt;instrument()&lt;/code&gt; once per process: it wires a &lt;code&gt;ParentBased(ALWAYS_ON)&lt;/code&gt; provider, a batching OTLP exporter, W3C trace context + baggage, and the price table. Then decorators wrap functions you already have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;taraol&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="nf"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;  &lt;span class="c1"&gt;# execute_tool span
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="nd"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# chat span (gen_ai semconv) + cost rollup
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;think&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# invoke_agent span around the step
&lt;/span&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;think&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;search&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="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function bodies stay your normal SDK calls. No wrappers, no monkey patching, no framework taking ownership of your app. Taraol instruments &lt;strong&gt;OpenTelemetry&lt;/strong&gt;, not a framework, so the same telemetry goes to SigNoz today and any other OTel backend tomorrow.&lt;/p&gt;

&lt;p&gt;Return the raw SDK response from a &lt;code&gt;@chat&lt;/code&gt; function and usage is read straight off it: google-genai, OpenAI-compatible, Anthropic, and flat result shapes are all duck-typed. No manual token counting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content capture is off by default.&lt;/strong&gt; No prompt, completion, or tool text leaves your process unless you explicitly opt in. A test asserts &lt;code&gt;gen_ai.input.messages&lt;/code&gt; is absent on the default path: the privacy claim is enforced, not documented.&lt;/p&gt;

&lt;h3&gt;
  
  
  The map draws itself
&lt;/h3&gt;

&lt;p&gt;Here's the part I find genuinely delightful, and the reason this is built on SigNoz rather than a bespoke dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no topology renderer in this project.&lt;/strong&gt; Not one line.&lt;/p&gt;

&lt;p&gt;SigNoz's Service Map is derived from two things ordinary distributed tracing already gives you: span parent/child relationships and &lt;code&gt;service.name&lt;/code&gt;. Make each agent its own service, propagate trace context on every hop, and the agent mesh &lt;em&gt;is&lt;/em&gt; a service map. It renders for free, reflects real production topology, and changes as traffic changes.&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%2Fmf51lw84k8n7vb5ykh1s.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%2Fmf51lw84k8n7vb5ykh1s.png" alt="The Taraol Service Map in SigNoz"&gt;&lt;/a&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%2Fn61q7xv5wuhw80h2ck4t.gif" 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%2Fn61q7xv5wuhw80h2ck4t.gif" alt="How the mesh assembles, then closes into a cycle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Diagram: the topology forming, then the writer ↔ critic revision loop closing it.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The one setting that will bite you
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sampler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ParentBased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ALWAYS_ON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each agent samples independently, agent three drops a trace that agents one and two kept. You get a &lt;em&gt;shredded&lt;/em&gt; mesh: edges that flicker, cycles that never close, cost that doesn't add up. &lt;code&gt;ParentBased&lt;/code&gt; means a child never re-decides: it inherits the root's&lt;br&gt;
decision. Get this wrong and every feature below silently degrades.&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%2F7kma072i3p6gaq2iyjpu.gif" 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%2F7kma072i3p6gaq2iyjpu.gif" alt="One trace id across five processes"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Cost, without double-counting
&lt;/h3&gt;

&lt;p&gt;This one took a bug to get right. Cost splits into two attributes that mean different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentmesh.cost.direct_usd&lt;/code&gt;&lt;/strong&gt;: one agent's own chat call. Additive across a conversation; sum these for the true total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agentmesh.cost.downstream_usd&lt;/code&gt;&lt;/strong&gt;: on a hop span, the cost of the callee's &lt;em&gt;entire subtree&lt;/em&gt;. This is per-delegation attribution (which path drives spend) and is explicitly &lt;strong&gt;not&lt;/strong&gt; additive across edges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sum both and you double-count the whole conversation. The bundled dashboards use &lt;code&gt;direct_usd&lt;/code&gt; for per-agent and per-conversation cost,&lt;code&gt;downstream_usd&lt;/code&gt; for the per-edge view.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Debug: replay the conversation from telemetry
&lt;/h2&gt;

&lt;p&gt;When something goes wrong you don't want a trace ID, you want the transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;taraol explain &amp;lt;trace-id&amp;gt; &lt;span class="nt"&gt;--replay&lt;/span&gt;
&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;[2] researcher   (113-&amp;gt;21 tok, $0.0001)
    input:  You are the researcher agent (step 1). ...
    output: Tracing matters because it delivers crucial end-to-end visibility...
    tool search_sources: -&amp;gt; [3 results]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's reconstructed from trace rows, not from an application-side log the app had to remember to write. The wire format stays standard &lt;code&gt;gen_ai.*&lt;/code&gt; message lists, so any OTel-aware tool can read it; the renderer just decodes it to something human.&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%2Fetm7vnq265dae6m7rjul.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%2Fetm7vnq265dae6m7rjul.png" alt="A research-mesh trace in SigNoz"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Defend: detection that does something
&lt;/h2&gt;

&lt;p&gt;This is the beat I care about most. Detection that only produces a notification still needs a human awake at 3am. Taraol's detection reads telemetry and &lt;strong&gt;acts&lt;/strong&gt;: it contains the failure, then &lt;em&gt;heals&lt;/em&gt; the moment the cause clears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Agents ──&amp;gt; OpenTelemetry ──&amp;gt; SigNoz ──&amp;gt; Watcher ──&amp;gt; Controller
                                              │            │
                                       loop_detected   pause agent
                                       edge_unhealthy  break edge
&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%2Fp1ri8p25r9dqb5e3fvke.gif" 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%2Fp1ri8p25r9dqb5e3fvke.gif" alt="Loop detected, edge cut, agent paused"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  "But doesn't my framework already cap loops?"
&lt;/h3&gt;

&lt;p&gt;Some do. LangGraph has &lt;code&gt;recursion_limit&lt;/code&gt;, CrewAI has &lt;code&gt;max_iter&lt;/code&gt;, AutoGen has &lt;code&gt;max_turns&lt;/code&gt;. They're all the same shape: an &lt;strong&gt;in-process counter, per framework, that aborts the whole run&lt;/strong&gt; when it trips. Three things they can't do, and all three are this demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See across processes.&lt;/strong&gt; A counter inside LangGraph can't see a loop where a LangGraph agent calls a plain FastAPI agent calls a CrewAI agent. Taraol reads the &lt;em&gt;traces&lt;/em&gt; across all of them, framework- and language-agnostic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell stuck from productive.&lt;/strong&gt; A counter fires at N whether the loop made progress or not. Taraol flags only &lt;em&gt;no-progress&lt;/em&gt; loops (a repeated state hash), so a legitimate generator/critic iteration survives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contain surgically, then recover.&lt;/strong&gt; A framework raises an exception and the whole run dies. Taraol cuts the one bad edge, keeps the rest of the mesh running, and self-heals when the edge  recovers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you build on raw SDKs (Gemini, OpenAI, Anthropic), there's no loop control at all. Taraol works the same either way, because it lives in the telemetry, not the control flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the watcher sees what no single agent can
&lt;/h3&gt;

&lt;p&gt;The watcher is a standalone service. It never imports your agents; it queries SigNoz on an interval (the v5 Query API when an API key is set, or ClickHouse directly for local Community). Each poll it pulls the recent &lt;code&gt;a2a.call&lt;/code&gt; hop spans and groups them by&lt;br&gt;
&lt;code&gt;(trace_id, src, peer)&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;velocity_query:  GROUP BY trace_id, agentmesh.src, peer.service  →  count() AS hops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A high &lt;code&gt;hops&lt;/code&gt; on one edge is the fingerprint of a loop, but a count alone isn't an incident, because a generator/critic pair &lt;em&gt;should&lt;/em&gt; iterate. So before flagging, the watcher fetches the trace and confirms &lt;strong&gt;no progress&lt;/strong&gt;: either the same agent emitted an identical&lt;br&gt;
&lt;code&gt;agentmesh.state.hash&lt;/code&gt; twice (it's re-deriving the same result), or a hard iteration cap was breached. Only then does it emit an ordinary, trace-correlated OpenTelemetry log record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;loop_detected&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;experiment_variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;trace_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pathology lived in the &lt;em&gt;shape of the traffic between processes&lt;/em&gt;; that grouped query is what reconstructs it. Cross-conversation loops (an agent ping-ponging across separate traces) get their own signal, &lt;code&gt;xconv_loop_detected&lt;/code&gt;, from the union of edges across conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-healing: the per-edge circuit breaker
&lt;/h3&gt;

&lt;p&gt;Sometimes the agent is fine and only one path is bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planner ────────► Writer ──X──► Critic
Researcher ─────► Writer            (only this edge is cut)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each edge gets its own breaker, a small state machine that short-circuits &lt;em&gt;before&lt;/em&gt; dispatch, so a runaway or poisoned hop stops flowing while the rest of the mesh keeps working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          record_failure × N (threshold)
  CLOSED ───────────────────────────────────▶ OPEN
    ▲                                            │  reset_timeout elapsed
    │ trial call succeeds                        ▼
    └──────────────────  HALF_OPEN  ◀────────────┘
                            │  one trial hop; fail → OPEN again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the self-healing part. An OPEN edge doesn't stay dead: after &lt;code&gt;reset_timeout&lt;/code&gt; the breaker moves to HALF_OPEN and lets &lt;strong&gt;one&lt;/strong&gt; trial hop through. It succeeds → back to CLOSED, the edge is restored &lt;strong&gt;automatically&lt;/strong&gt;; it fails → straight back to OPEN. No operator, no restart: the system cuts the bad edge, waits, and re-tests it on its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;taraol.breaker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edge_key&lt;/span&gt;

&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;edge_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&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;critic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;get_registry&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;     &lt;span class="c1"&gt;# False while OPEN: the runaway hop can't continue
&lt;/span&gt;    &lt;span class="p"&gt;...&lt;/span&gt;                            &lt;span class="c1"&gt;# dispatch, then record_success() / record_failure()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blast radius stays minimal (one edge, not the whole agent going dark), and recovery needs no human in the loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injection taint
&lt;/h3&gt;

&lt;p&gt;Agents exchange natural language, so a malicious prompt propagates like a contagion. Scan an input, mark the span, and the taint spreads downstream automatically via OTel baggage:&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;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetched_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INPUT&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;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_injection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# spreads via baggage
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question changes from &lt;em&gt;"was this agent attacked?"&lt;/em&gt; to &lt;em&gt;"which agents consumed poisoned information?"&lt;/em&gt;, and you read the blast radius off SigNoz. The &lt;code&gt;injection_detected&lt;/code&gt; signal carries the origin and the comma-joined set of services the taint reached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcement: SigNoz owns the policy, the controller acts
&lt;/h3&gt;

&lt;p&gt;The breaker heals a &lt;em&gt;local&lt;/em&gt; edge on its own. For a distributed loop, detection and enforcement are deliberately separated, with SigNoz as the policy boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loop_detected log
   └▶ SigNoz alert rule            ← you set the threshold and when it matters
        └▶ webhook → Controller     ← POST from SigNoz's alertmanager
             └▶ pause agent / break edge
                  └▶ agent_paused / edge_broken  ──▶ back to SigNoz (audit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;watcher&lt;/strong&gt; decides the telemetry is &lt;em&gt;suspicious&lt;/em&gt;. &lt;strong&gt;SigNoz&lt;/strong&gt; decides whether that should trigger a response: a visible, editable alert rule, not logic buried inside a detector. The &lt;strong&gt;controller&lt;/strong&gt; performs the action and records it as its own telemetry. &lt;strong&gt;Nothing happens silently&lt;/strong&gt;, which is the only version of automated enforcement I'd actually run in production.&lt;/p&gt;

&lt;p&gt;Every signal is an ordinary OpenTelemetry log record, searchable next to the trace it points at:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;signal&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;loop_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a conversation is looping without progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;edge_unhealthy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one agent→agent edge is over its hop budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;budget_exceeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a conversation blew its cost budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;injection_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;taint found, with origin + blast radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xconv_loop_detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a loop spread across separate conversations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;agent_paused&lt;/code&gt; / &lt;code&gt;edge_broken&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;controller audit events after enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No separate monitoring system. No custom database. It's all SigNoz.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Improve: which design should I actually deploy?
&lt;/h2&gt;

&lt;p&gt;Observability tells you what happened. It doesn't tell you what to ship. &lt;strong&gt;AgentLab&lt;/strong&gt; runs the same workload as tagged variants and compares them on &lt;strong&gt;operational&lt;/strong&gt; telemetry (cost, latency, loops, breaker trips, failures), &lt;em&gt;not&lt;/em&gt; answer quality. That boundary is deliberate: it tells you which design is cheaper, faster, and more stable to &lt;em&gt;run&lt;/em&gt;, which complements a quality eval rather than replacing it.&lt;/p&gt;

&lt;p&gt;Start with the question every team has this quarter: &lt;strong&gt;Gemini 3, GPT-5, or Claude?&lt;/strong&gt; A variant is just configuration, so a model bake-off is a few lines, and the same &lt;code&gt;@chat&lt;/code&gt; reads token usage off all three providers' response shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;taraol&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;

&lt;span class="c1"&gt;# one @chat per provider; @chat reads token usage off each SDK's own response shape
&lt;/span&gt;&lt;span class="nd"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_gpt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;

&lt;span class="nd"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;

&lt;span class="n"&gt;PROVIDERS&lt;/span&gt; &lt;span class="o"&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;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ask_gemini&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ask_gpt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ask_claude&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PROVIDERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;TASK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# the variant picks the provider
&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-bakeoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gemini 3 vs GPT-5 vs Claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fraol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a table like this (numbers illustrative), and now you're choosing on cost, latency, and stability instead of vibes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;variant   cost$    tokens   avg ms   fails   health
gemini    0.0009    1240      1800      0       98
gpt5      0.0180    1310      4200      0       95
claude    0.0110    1180      3100      0       96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat worth stating: put each model's pricing in the price table (bundled, or via &lt;code&gt;OAK_PRICING_FILE&lt;/code&gt;), or the cost column reads &lt;em&gt;unpriced&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Because a variant is &lt;em&gt;just config&lt;/em&gt;, anything that changes the run is comparable with the same fluent API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dimension&lt;/th&gt;
&lt;th&gt;knob&lt;/th&gt;
&lt;th&gt;the question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provider="gpt5"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cheaper / faster / more stable?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prompt="…"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;terse vs verbose cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temperature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;temperature=0.7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;does sampling raise cost or loops?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;topology&lt;/td&gt;
&lt;td&gt;&lt;code&gt;use_critic=False&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;does a reviewer reduce loops, or just add cost?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;search=True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;does grounding change cost / latency?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loop mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loop_mode="storm"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;converge vs runaway safety&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prompt A/B is the same call, a different knob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;taraol&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Experiment&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs-assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fraol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain OpenTelemetry in exactly one sentence.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain OpenTelemetry in three detailed paragraphs.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;broken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# a failing variant is recorded, not fatal
&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every span emitted during a variant carries &lt;code&gt;experiment.id&lt;/code&gt;, &lt;code&gt;experiment.variant&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;experiment.run_id&lt;/code&gt;, &lt;strong&gt;including cross-process hop spans&lt;/strong&gt;. So SigNoz becomes the&lt;br&gt;
experiment dashboard, with no new UI and no separate store.&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%2Figk3u2sb9uaduwtvdnk4.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%2Figk3u2sb9uaduwtvdnk4.png" alt="The AgentLab comparison dashboard in SigNoz"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The baseline-vs-runaway table at the top of this post came from exactly this, run over the mesh.&lt;br&gt;
Its &lt;code&gt;loops&lt;/code&gt; and &lt;code&gt;breakers&lt;/code&gt; columns aren't instrumented by the experiment; they come from the &lt;strong&gt;watcher detecting the storm in telemetry&lt;/strong&gt;, with the signals carrying the variant that caused&lt;br&gt;
them. Defend and Improve are the same data read two ways.&lt;/p&gt;

&lt;p&gt;The health score is deliberately transparent and overridable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;health = 100 - 10·loops - 5·breaker_trips - 0.2·latency_s - 0.1·cost_usd - 20·failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Highest Operational Health" is a factual read against weights &lt;em&gt;you&lt;/em&gt; set, not a universal winner. One team optimises cost, another latency, another stability. A tool that hard-codes that tradeoff is lying to somebody.&lt;/p&gt;

&lt;p&gt;Small design detail I'd defend: a variant that raises is recorded &lt;code&gt;status=failed&lt;/code&gt; and the run continues. An experiment runner that aborts on one failure is useless precisely when you need it.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"taraol[all]"&lt;/span&gt;
taraol signoz up          &lt;span class="c"&gt;# local SigNoz, or point at SigNoz Cloud&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two demos, deliberately at different scales:&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;# 4 agents, one process, real Gemini: decorators, an experiment,&lt;/span&gt;
&lt;span class="c"&gt;# a revision loop that trips a breaker, a guardrail catching an injection&lt;/span&gt;
uv run python demos/trip_planner/app.py

&lt;span class="c"&gt;# 5 services in containers: cross-process traces, Service Map,&lt;/span&gt;
&lt;span class="c"&gt;# watcher detection, alert → controller enforcement&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; demos/research_mesh/compose.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;research-mesh-planner-1 python &lt;span class="nt"&gt;-m&lt;/span&gt; research_mesh.experiment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Honest edges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replay requires opting into content capture.&lt;/strong&gt; That's the right default, but it means the transcript isn't there retroactively for an incident you didn't anticipate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection analyses telemetry, so it isn't instantaneous.&lt;/strong&gt; It's bounded by ingest and analysis, not by an in-process hook. The tradeoff buys framework independence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentLab's summary needs the &lt;code&gt;[detection]&lt;/code&gt; extra&lt;/strong&gt; and a few seconds for batched telemetry to land before the numbers are complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The health weights are a heuristic.&lt;/strong&gt; They're a starting point you're expected to tune, not a benchmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost accuracy is bounded by the price table.&lt;/strong&gt; Update it when providers change pricing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you run more than one agent, the useful question isn't "what did this agent do." It's &lt;strong&gt;"what is the shape of the traffic between them, what is that shape costing me, and can the system cut it before I wake up."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can answer all three today without a proprietary SDK, by doing two boring things well: give every agent its own &lt;code&gt;service.name&lt;/code&gt;, and propagate &lt;code&gt;traceparent&lt;/code&gt; on every hop. SigNoz draws the rest, and once the telemetry is good enough to draw the map, it's good enough to defend it.&lt;/p&gt;

&lt;p&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/MTC-123/Taraol" rel="noopener noreferrer"&gt;github.com/MTC-123/Taraol&lt;/a&gt;&lt;/strong&gt;. Apache-2.0,&lt;br&gt;
framework-neutral, vendor-neutral, content-free by default.&lt;/p&gt;

&lt;p&gt;Built on &lt;a href="https://signoz.io/" rel="noopener noreferrer"&gt;SigNoz&lt;/a&gt; and &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Submitted to the Agents of SigNoz hackathon, Track 01, AI &amp;amp; Agent Observability.&lt;/em&gt;&lt;br&gt;
ok&lt;/p&gt;

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