<?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: Himanshu Kumar</title>
    <description>The latest articles on DEV Community by Himanshu Kumar (@himanshu_748).</description>
    <link>https://dev.to/himanshu_748</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%2F3226847%2F8b999f0b-76e8-4a5b-a87d-449ab82832bc.png</url>
      <title>DEV Community: Himanshu Kumar</title>
      <link>https://dev.to/himanshu_748</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/himanshu_748"/>
    <language>en</language>
    <item>
      <title>Tracing a multi-agent LLM system: otel-swarm and a SigNoz dashboard pack</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:40:05 +0000</pubDate>
      <link>https://dev.to/himanshu_748/tracing-a-multi-agent-llm-system-otel-swarm-and-a-signoz-dashboard-pack-4m85</link>
      <guid>https://dev.to/himanshu_748/tracing-a-multi-agent-llm-system-otel-swarm-and-a-signoz-dashboard-pack-4m85</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A single LLM call is easy to reason about. You send a prompt, you get tokens back, you log the latency and you move on.&lt;/p&gt;

&lt;p&gt;A swarm is not that. Four or five agents run, some in parallel, each with its own model. One of them times out and quietly falls back to a cheaper model. A critic agent reads the output and sends work back for another round. When the whole thing takes 40 seconds instead of 12, or costs three times what you budgeted, you have no idea which agent did it. Your logs are a flat stream of interleaved lines from concurrent tasks, with no parent-child structure and no way to ask "which role burned the tokens".&lt;/p&gt;

&lt;p&gt;Traces solve this exactly. The catch is that hand-wiring OpenTelemetry across every role, every provider call and every retry path is boring work that nobody wants to do twice, and if you skip a level the trace tree lies to you.&lt;/p&gt;

&lt;p&gt;So I extracted the instrumentation out of DevSwarm (a multi-agent code generator I built for this hackathon) into a standalone MIT library: &lt;a href="https://github.com/himanshu748/otel-swarm" rel="noopener noreferrer"&gt;otel-swarm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;createSwarm()&lt;/code&gt; call, then three verbs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSwarm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;otel-swarm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;swarm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSwarm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-swarm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;otlpEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4318&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my.prompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;planner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;planner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary-model-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fallbackModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fallback-model-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yourProviderCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reviewEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// each issue becomes a critic_catch span event&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* llm_start, llm_end, fallback, critic_catch */&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;task()&lt;/code&gt; opens the root span, &lt;code&gt;agent()&lt;/code&gt; opens a child span per role and &lt;code&gt;llm()&lt;/code&gt; opens a GenAI-semconv span per model call. &lt;code&gt;call(model)&lt;/code&gt; is invoked again with &lt;code&gt;fallbackModel&lt;/code&gt; if the primary throws, and the switch is recorded as a &lt;code&gt;fallback_promotion&lt;/code&gt; span event carrying &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt; and the verbatim provider error.&lt;/p&gt;

&lt;p&gt;The last line matters more than it looks. &lt;code&gt;swarm.events&lt;/code&gt; is an EventEmitter that mirrors the span lifecycle, and LLM events carry the &lt;code&gt;traceId&lt;/code&gt;. A live UI reads the emitter, the tracing backend reads the OTLP exporter and both are fed from the same code path, so the dashboard and the UI can never disagree about what happened. The &lt;code&gt;traceId&lt;/code&gt; means a row in your UI deep-links to the exact trace in SigNoz.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;npm run example&lt;/code&gt; against a local SigNoz produced 9 spans under service &lt;code&gt;otel-swarm-demo&lt;/code&gt;: a root &lt;code&gt;generation&lt;/code&gt; span of 1325ms, then &lt;code&gt;agent.planner&lt;/code&gt;/&lt;code&gt;llm.planner&lt;/code&gt;, &lt;code&gt;agent.frontend&lt;/code&gt;/&lt;code&gt;llm.frontend&lt;/code&gt;, &lt;code&gt;agent.backend&lt;/code&gt;/&lt;code&gt;llm.backend&lt;/code&gt; and &lt;code&gt;agent.critic&lt;/code&gt;/&lt;code&gt;llm.critic&lt;/code&gt;. &lt;code&gt;llm.frontend&lt;/code&gt; carried a &lt;code&gt;fallback_promotion&lt;/code&gt; event and &lt;code&gt;agent.critic&lt;/code&gt; carried a &lt;code&gt;critic_catch&lt;/code&gt; event. The whole story is one trace.&lt;/p&gt;

&lt;p&gt;In production use inside DevSwarm the same library has traced 29 generations, 243 model calls across 8 models, 3.78 million tokens and 257 review catches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: put the fallback in a span event, never on the attribute you group by
&lt;/h2&gt;

&lt;p&gt;This one cost me an afternoon.&lt;/p&gt;

&lt;p&gt;An earlier version of the library did the obvious thing when a primary model failed: it overwrote &lt;code&gt;gen_ai.request.model&lt;/code&gt; with the fallback's name, so the span would "tell the truth" about which model actually answered.&lt;/p&gt;

&lt;p&gt;That is wrong, and it is wrong in a way that is hard to see. Every dashboard panel that groups by model then attributes the primary's failure, its timeout and all of its wasted latency to the fallback that cleaned up after it. My "tokens and latency by model" table showed a cheap fallback model with terrible p95 latency and my expensive primary looking flawless, because every time the primary blew up its cost was silently reassigned to whoever picked up the pieces. I spent an afternoon convinced the wrong model was slow.&lt;/p&gt;

&lt;p&gt;The rule that comes out of it: &lt;strong&gt;the attribute you group by must record the model you attempted, not the model that ended up answering.&lt;/strong&gt; The promotion is a discrete thing that happened during the span, and a discrete thing that happened during a span is what span events are for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fallback_promotion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallbackModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&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;Now the grouped panel keeps attributing failure to whoever caused it, and "how often does this role get promoted" is a separate query over events. Two different questions, two different storage locations, no cross-contamination.&lt;/p&gt;

&lt;p&gt;The general form: if a value can change mid-span, it does not belong on an attribute you aggregate over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: reading inside span events in ClickHouse is not obvious
&lt;/h2&gt;

&lt;p&gt;The consequence of putting things in events is that you now have to query events, and SigNoz stores them as an array of JSON strings on the span row. There is no autocomplete that will lead you here. The pattern is &lt;code&gt;ARRAY JOIN&lt;/code&gt; to flatten the array into one row per event, then &lt;code&gt;JSONExtractString&lt;/code&gt; to reach into the event's &lt;code&gt;attributeMap&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attributeMap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'severity'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attributeMap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'target'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;catches&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{.service}}'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent.critic'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'critic_catch'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;catches&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you review findings broken down by severity and by what they were aimed at, straight out of span events, with no separate metrics pipeline.&lt;/p&gt;

&lt;p&gt;When you only need to know whether an event fired at all, do not pay for the &lt;code&gt;ARRAY JOIN&lt;/code&gt;. &lt;code&gt;arrayExists&lt;/code&gt; with a substring test over the raw array is enough and it keeps one row per span, which is what you want for a time series:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;toStartOfInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;attributes_string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'swarm.role'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{.service}}'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;arrayExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%fallback_promotion%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;{{.&lt;/span&gt;&lt;span class="n"&gt;start_datetime&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;{{.&lt;/span&gt;&lt;span class="n"&gt;end_datetime&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The dashboard pack
&lt;/h2&gt;

&lt;p&gt;The repo ships 3 importable SigNoz dashboards (POST each JSON to &lt;code&gt;/api/v1/dashboards&lt;/code&gt;). Every query uses a &lt;code&gt;{{.service}}&lt;/code&gt; dashboard variable that defaults to &lt;code&gt;otel-swarm-demo&lt;/code&gt;, so you point the pack at your own service by editing one dropdown instead of doing find-and-replace across query strings.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generation Overview&lt;/strong&gt; answers "is the swarm healthy right now": generation count, review-gate pass rate, seconds per generation over time and a per-role table of calls, average latency, p95 and errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Economics&lt;/strong&gt; answers "where did the tokens go": total tokens, tokens over time by role, a tokens-and-latency table by model and fallback promotions by role (the &lt;code&gt;arrayExists&lt;/code&gt; query above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review-Gate Funnel&lt;/strong&gt; answers "is the reviewer doing its job": total catches, regeneration rounds, catches per generation over time and a table of recent generations with verdict, catches and regenerations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The alert rules
&lt;/h2&gt;

&lt;p&gt;Two rules ship in the v2alpha1 schema (POST to &lt;code&gt;/api/v2/rules&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The first is a fallback-promotion spike. If promotions jump, a provider is degrading and you are silently paying a different bill than you planned.&lt;/p&gt;

&lt;p&gt;The second is the one I care about more: a &lt;strong&gt;review catch-rate flatline&lt;/strong&gt;. If your critic agent suddenly stops finding anything, the tempting read is that the generators got better. In practice the reviewer broke: a prompt change made it answer in a shape the parser drops, or its model started returning empty content and the failure is being swallowed as "no issues found". A quality gate that passes everything is indistinguishable from no quality gate, and it fails silently by construction. Alert on the absence.&lt;/p&gt;

&lt;p&gt;The repo also includes &lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt;, the Foundry config the SigNoz instance behind these dashboards was installed from, so the backend the pack targets is reproducible rather than assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;github:himanshu748/otel-swarm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the example trace end to end against a local SigNoz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/himanshu748/otel-swarm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;otel-swarm
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run example                                                     &lt;span class="c"&gt;# spans to console&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4318 npm run example   &lt;span class="c"&gt;# spans to SigNoz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no endpoint set it exports to the console, so you can check the span tree before wiring a backend.&lt;/p&gt;

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

&lt;p&gt;The library is about 130 lines. Most of the value is not code, it is the two decisions above: keep mutable facts in span events, and treat "the reviewer found nothing" as a symptom rather than a result. Both were learned by getting them wrong first.&lt;/p&gt;

&lt;p&gt;I built otel-swarm with Claude Code, including the dashboard JSON and the ClickHouse queries in this post.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/himanshu748/otel-swarm" rel="noopener noreferrer"&gt;github.com/himanshu748/otel-swarm&lt;/a&gt;, MIT.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>We instrumented an AI agent swarm with SigNoz, and its own telemetry told us we were wrong about almost everything</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 25 Jul 2026 15:01:20 +0000</pubDate>
      <link>https://dev.to/himanshu_748/we-instrumented-an-ai-agent-swarm-with-signoz-and-its-own-telemetry-told-us-we-were-wrong-about-3fip</link>
      <guid>https://dev.to/himanshu_748/we-instrumented-an-ai-agent-swarm-with-signoz-and-its-own-telemetry-told-us-we-were-wrong-about-3fip</guid>
      <description>&lt;p&gt;Built for the WeMakeDevs Agents of SigNoz hackathon, July 2026.&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%2F9sslzrvxjmpqz4unvdon.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%2F9sslzrvxjmpqz4unvdon.png" alt="DevSwarm Mission Control: the swarm graph, the live trace river and the hangar of everything it has built" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Mission Control. The graph is the swarm, the river underneath it is the live span stream, and every bar deep-links into that trace in SigNoz.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DevSwarm turns one prompt into a working full-stack app. Five open-weight models plan it, build it, review it and repair their own routing. Nothing it produces is trusted blindly, and every step is an OpenTelemetry span in SigNoz, including the steps that go wrong.&lt;/p&gt;

&lt;p&gt;We built the observability first, expecting it to prove the thing worked.&lt;/p&gt;

&lt;p&gt;It did something more useful. It spent a week proving that almost everything we believed about our own system was wrong. We blamed a model for a limit we had set ourselves. We blamed a provider outage on the model. We assumed our review agent was our strongest link when it was measurably the weakest. And we spent days writing a design system that, when we finally measured it, was making the output worse.&lt;/p&gt;

&lt;p&gt;Not one of those was found by reading the code again. Every single one came off a span event, a dashboard row or a benchmark that disagreed with us.&lt;/p&gt;

&lt;p&gt;So this is not an architecture post. It is six times the telemetry told us we were wrong, with the queries.&lt;/p&gt;

&lt;p&gt;The current numbers, all read live out of SigNoz rather than typed into a slide: 22 generations, 189 traced model calls across 8 models, 2.85 million tokens, 225 critic catches, 19 fallback promotions and 24 generated apps each reporting under their own service name.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the swarm actually is
&lt;/h2&gt;

&lt;p&gt;Five roles, each on the open-weight model that measured best for that job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;planner&lt;/td&gt;
&lt;td&gt;GLM-5.2&lt;/td&gt;
&lt;td&gt;turn a prompt into a typed build plan and a locked API contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;frontend&lt;/td&gt;
&lt;td&gt;GLM-5.2&lt;/td&gt;
&lt;td&gt;one self-contained index.html against that contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend&lt;/td&gt;
&lt;td&gt;Qwen3-Coder-480B&lt;/td&gt;
&lt;td&gt;one Express server against the same contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;critic&lt;/td&gt;
&lt;td&gt;Kimi-K2.7-Code&lt;/td&gt;
&lt;td&gt;review both, gate the merge, route catches back to their owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;doctor&lt;/td&gt;
&lt;td&gt;GLM-5.2&lt;/td&gt;
&lt;td&gt;read the swarm's own traces and repair its model routing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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%2Fgwxc8ylw20rwodipyfe7.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%2Fgwxc8ylw20rwodipyfe7.png" alt="The DevSwarm landing page" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every number on our own landing page is a live ClickHouse query against the trace store. Marketing copy that drifts from the telemetry is impossible by construction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything is served through Hugging Face Inference Providers. There are zero closed-model API calls in the system, which turned out to matter for reasons we did not anticipate (see the provider section below).&lt;/p&gt;

&lt;p&gt;The critic is the load-bearing part. Frontend and backend are generated in parallel from the same contract, then an independent model reviews both for contract conformance, security and runtime bugs. Real catches route back to the agent that owns them, that agent patches its own file and the critic re-reviews only the delta. Two regeneration rounds, then it ships with an honest verdict either way.&lt;/p&gt;

&lt;p&gt;The same gate runs on changes. Asking a finished app for "a star rating on each book, settable when adding one" re-plans against the stored contract, re-runs only the agents that instruction touches, and puts the result through the identical review. A refinement is its own root span, so a change request is as traceable after the fact as the build that preceded it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why we instrumented before we polished
&lt;/h2&gt;

&lt;p&gt;A multi-agent system fails in ways a single-model tool does not. A call can succeed while producing an unusable artifact. A fallback can rescue a request so smoothly that nobody notices the primary is dead. Latency can triple because one role silently started thinking twice as long. None of that shows up in a request log.&lt;/p&gt;

&lt;p&gt;So the very first thing that worked in this project was not code generation. It was a trace.&lt;/p&gt;

&lt;p&gt;SigNoz is self-hosted through Foundry, which is a single-config install. Our &lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt; are committed to the repo so the deployment is reproducible by anyone, judges included.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three signals, and what each one carries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traces.&lt;/strong&gt; Every model call is a span named &lt;code&gt;llm.&amp;lt;role&amp;gt;&lt;/code&gt; carrying GenAI semantic conventions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.operation.name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;devswarm.role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two span events do the heavy diagnostic lifting. &lt;code&gt;fallback_promotion&lt;/code&gt; records that a primary failed, which model took over and the verbatim reason. &lt;code&gt;critic_catch&lt;/code&gt; records every issue the review agent found, with its target and severity. Both are events rather than separate spans on purpose: they belong to the call they describe, and they survive in the trace even when the call ultimately succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics.&lt;/strong&gt; Six counters and a histogram, because some questions are time series questions rather than trace questions: &lt;code&gt;devswarm.tokens&lt;/code&gt;, &lt;code&gt;devswarm.llm.calls&lt;/code&gt;, &lt;code&gt;devswarm.llm.duration&lt;/code&gt;, &lt;code&gt;devswarm.fallback.promotions&lt;/code&gt;, &lt;code&gt;devswarm.critic.catches&lt;/code&gt;, &lt;code&gt;devswarm.generations&lt;/code&gt;, &lt;code&gt;devswarm.refinements&lt;/code&gt;. Labelled by role, model and outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs.&lt;/strong&gt; Structured records for the things a human reads during an incident: a fallback promoting, a doctor diagnosis, a generation completing with its verdict and catch count. Same resource attributes as the traces, so a log line and a span line up.&lt;/p&gt;

&lt;p&gt;The whole trace layer is now extracted into a small library, &lt;a href="https://github.com/himanshu748/otel-swarm" rel="noopener noreferrer"&gt;otel-swarm&lt;/a&gt;, that any multi-agent system can drop in. DevSwarm consumes it as a real dependency, which means if the library breaks, our own dashboards go dark first. That felt like the honest way to ship it.&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%2Ft1bxuwby7j6yseftcvue.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%2Ft1bxuwby7j6yseftcvue.png" alt="A generation trace in SigNoz, nested agent and llm spans" width="800" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One generation as a flame graph. Planner, then frontend and backend in parallel, then the critic.&lt;/em&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%2Fe0bn6up4gc619owsbk5g.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%2Fe0bn6up4gc619owsbk5g.png" alt="Structured logs in SigNoz showing fallback promotions and generation verdicts" width="800" height="445"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The log stream during a rough run. WARN lines are fallback promotions, each naming the model that failed and why.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the swarm out of ClickHouse
&lt;/h2&gt;

&lt;p&gt;Five dashboards, all committed as JSON in &lt;code&gt;observability/dashboards/&lt;/code&gt;. The one we actually live in is Command Center: is the swarm healthy, and if not, which role. The odd one out is Born Observable, which contains no swarm data at all, only the generated apps reporting under their own service names.&lt;/p&gt;

&lt;p&gt;Three queries worth sharing, because span events in ClickHouse are not obvious the first time.&lt;/p&gt;

&lt;p&gt;Role health, straight off the trace store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attributes_string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'devswarm.role'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;durationNano&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;e9&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;AS&lt;/span&gt; &lt;span class="n"&gt;p95_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes_number&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gen_ai.usage.input_tokens'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;attributes_number&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gen_ai.usage.output_tokens'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'devswarm'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'llm.%'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fallback promotions over time, which requires reaching into the events array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;toStartOfInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;attributes_string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'devswarm.role'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'devswarm'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;arrayExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%fallback_promotion%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one that took longest to work out reads &lt;em&gt;inside&lt;/em&gt; the events, so you can ask what the review gate actually caught rather than how many times it caught something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attributeMap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'severity'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attributeMap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'target'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;catches&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;signoz_traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distributed_signoz_index_v3&lt;/span&gt;
&lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'devswarm'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent.critic'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;JSONExtractString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'critic_catch'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;catches&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer, over the week: 221 high, 46 medium, 10 low, and the frontend agent is on the receiving end of 70 percent of them. That single row changed how we think about the swarm. The half of the system generating markup and client-side state is where the bugs live, not the half touching the database.&lt;/p&gt;

&lt;p&gt;One design decision we are glad about: the marketing numbers on our own landing page are fetched from these same queries at request time. The page cannot drift from the telemetry, because there is only one source of both.&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%2Ft179tlrsj58ztorz3x7h.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%2Ft179tlrsj58ztorz3x7h.png" alt="The DevSwarm Command Center dashboard in SigNoz" width="799" height="419"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Command Center. Top row answers "is the swarm healthy", the role-health table answers "which role", while the fallback chart should trend to zero.&lt;/em&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%2Fbvwqe1g1qpins69dq5jf.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%2Fbvwqe1g1qpins69dq5jf.png" alt="The LLM Economics dashboard in SigNoz" width="800" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;LLM Economics: tokens and latency per role and per model, which is how we caught the frontend role burning two thirds of its budget on reasoning.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What one app actually costs
&lt;/h2&gt;

&lt;p&gt;Every token in the table below came off a span. This is one real run, the letterpress site above, priced at the rates the Hugging Face router itself reports for the providers we use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;in&lt;/th&gt;
&lt;th&gt;out&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;frontend&lt;/td&gt;
&lt;td&gt;GLM-5.2&lt;/td&gt;
&lt;td&gt;27,855&lt;/td&gt;
&lt;td&gt;38,888&lt;/td&gt;
&lt;td&gt;$0.2101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;critic&lt;/td&gt;
&lt;td&gt;Kimi-K2.7-Code&lt;/td&gt;
&lt;td&gt;50,996&lt;/td&gt;
&lt;td&gt;14,539&lt;/td&gt;
&lt;td&gt;$0.1066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;planner&lt;/td&gt;
&lt;td&gt;GLM-5.2&lt;/td&gt;
&lt;td&gt;670&lt;/td&gt;
&lt;td&gt;4,090&lt;/td&gt;
&lt;td&gt;$0.0189&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend&lt;/td&gt;
&lt;td&gt;Qwen3-Coder-480B&lt;/td&gt;
&lt;td&gt;2,800&lt;/td&gt;
&lt;td&gt;3,778&lt;/td&gt;
&lt;td&gt;$0.0069&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82,321&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;61,295&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.34&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Thirty four cents for a designed marketing site with a working Express backend, a waitlist that validates email and rejects duplicates, plus its own OpenTelemetry wiring. Across passing runs the range is about 18 cents to 56 cents.&lt;/p&gt;

&lt;p&gt;Two things in that table surprised us.&lt;/p&gt;

&lt;p&gt;The critic costs fifteen times what the backend author costs. Reviewing the code is dramatically more expensive than writing it, because review means reading both artifacts in full, twice, while the backend agent writes one file once. Nobody budgets for that. If you are building a review gate into an agent system, it is not a rounding error on top of generation, it is a third of your bill.&lt;/p&gt;

&lt;p&gt;And a failed run costs more than a successful one. Our worst generations burned 232,000 tokens hitting the regeneration ceiling, against 74,000 for the cleanest pass. So convergence is not only a quality metric, it is the cost metric. Fixing the contract-format bug in finding five did more for our unit economics than any model swap we made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerts that wake an agent instead of a human
&lt;/h2&gt;

&lt;p&gt;This is the part of the build we are proudest of, and it is a genuinely small amount of code.&lt;/p&gt;

&lt;p&gt;Two alert rules live in &lt;code&gt;observability/alerts/&lt;/code&gt;: a fallback-usage spike and a critic catch-rate flatline. Both notify a webhook channel called &lt;code&gt;swarm-doctor&lt;/code&gt;, which points at &lt;code&gt;POST /api/doctor/webhook&lt;/code&gt; on the swarm itself.&lt;/p&gt;

&lt;p&gt;When an alert fires, the Doctor wakes up, queries the swarm's own traces for the last hour and decides what to do about the routing table. It promotes a backup model, resets a recovered primary or does nothing, then explains itself in plain English using the numbers it just read. Its first real diagnosis, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The critic role is the clear problem area: its primary triggered 6 fallback promotions out of 9 calls (67%) with a 22% error rate and p95 latency of 242s. Backend, planner and doctor are healthy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Doctor's own model calls are traced too, so the healer is exactly as observable as the patient.&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%2Fsbfrpmda6t129jpmn3kp.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%2Fsbfrpmda6t129jpmn3kp.png" alt="Mission Control showing the Swarm Doctor's diagnosis panel" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Doctor reporting a healthy swarm. It read 180 minutes of its own traces to say so, and it is honest about sample size: "call volume is very low, so latency figures are not statistically meaningful".&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two hard-won SigNoz API notes, since we lost hours to both:&lt;/p&gt;

&lt;p&gt;Alert rules must be created against &lt;code&gt;/api/v2/rules&lt;/code&gt; with &lt;code&gt;schemaVersion: v2alpha1&lt;/code&gt;, a &lt;code&gt;notificationSettings&lt;/code&gt; block and at least one channel. The v1 endpoint accepts the request and returns &lt;code&gt;"alert rule is not valid"&lt;/code&gt; with no indication of which field is wrong. Dashboards, by contrast, go to &lt;code&gt;/api/v1/dashboards&lt;/code&gt; with a &lt;code&gt;SIGNOZ-API-KEY&lt;/code&gt; header and behave exactly as documented.&lt;/p&gt;

&lt;p&gt;Also: a cold Docker restart can leave ClickHouse replicas read-only until Keeper reconnects. It usually self-heals within a minute. If it does not, &lt;code&gt;SYSTEM RESTORE REPLICA&lt;/code&gt; per table clears it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps that are born observable
&lt;/h2&gt;

&lt;p&gt;Every app the swarm generates ships instrumented. Alongside &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;server.js&lt;/code&gt;, each generated folder gets an &lt;code&gt;otel.mjs&lt;/code&gt; bootstrap, a &lt;code&gt;package.json&lt;/code&gt;, and a &lt;code&gt;signoz-dashboard.json&lt;/code&gt; scoped to that app's own service name. If a &lt;code&gt;SIGNOZ_API_TOKEN&lt;/code&gt; is configured, the dashboard is created in SigNoz at generation time, before the user has opened the preview.&lt;/p&gt;

&lt;p&gt;So the generated app appears in SigNoz as its own service, with RED metrics and a routes table, seconds after it exists. Twenty four of them are in our instance right now.&lt;/p&gt;

&lt;p&gt;Worth saying because people assume otherwise: there is no image model anywhere in this pipeline. The swarm generates 227 inline SVG elements across the 26 apps it has built, an average of 8.7 per app, and every one of them was written as markup by a language model. The Vandercook press in the screenshot above is hand-drawn SVG, not a generated image. The only assets we ever image-generated are DevSwarm's own favicon and social card, which are branding for the tool rather than anything the swarm produces.&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%2Fforp67ml0koev7bulj1b.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%2Fforp67ml0koev7bulj1b.png" alt="Quoin and Roller, a letterpress site generated from one sentence" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One of the outputs. The Vandercook press is inline SVG the model drew itself, and this app reports to SigNoz under its own service name from the moment it boots.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One caveat we had to learn the hard way: an app only emits spans while its backend is actually running. Early on our preview served the frontend statically, so the generated Express server never booted and the app silently fell back to localStorage. The preview looked perfect and the service page was almost empty. That mismatch, two spans where there should have been dozens, is what gave the bug away. Previews now spawn the real server as a child process and proxy to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things our own telemetry told us we had wrong
&lt;/h2&gt;

&lt;p&gt;This is the section I would want to read, so it is the longest one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A "model limitation" was a stale constant we wrote ourselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traces showed every frontend failure as &lt;code&gt;finish: length&lt;/code&gt;, truncating full-page HTML. We concluded GLM-5.2's provider capped completions at 16384 tokens and moved the role to another model. The cap was real when we found it. It was also in our own config, and when the provider limit later lifted, our constant kept enforcing a limit that no longer existed. The average frontend artifact needs about 19,000 output tokens. We had guaranteed truncation and blamed the model for a fortnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The real cause was provider roulette, visible only in the span event text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After removing our own cap, GLM still failed intermittently with a 400: &lt;code&gt;max_completion_tokens is limited to 16384 for glm-5.2&lt;/code&gt;. The Hugging Face router load-balances a model across every provider serving it, and their limits disagree. We probed all seven: scaleway caps at 16384, featherless at 32768, novita and zai-org at 131072, while together, fireworks-ai and deepinfra accept 200000 or more. Unpinned, roughly one request in seven hit the strict provider and died instantly. Pinning the model to one provider produced our first ever generation with zero fallbacks. That entire diagnosis came out of the &lt;code&gt;reason&lt;/code&gt; attribute on a &lt;code&gt;fallback_promotion&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Our span attribute was hiding the failures we most wanted to see.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a primary failed, our code overwrote &lt;code&gt;gen_ai.request.model&lt;/code&gt; on the span with the fallback's name. It seemed tidy. It meant every dashboard row attributed the primary's failure, and its wasted latency, to the fallback that cleaned up after it. We spent an afternoon convinced the critic's backup model was slow and error-prone. Isolating them in a benchmark showed the opposite: the backup was fine at 7 seconds, and the primary was the problem. If you take one implementation detail from this post, take this one. Record the model you attempted, and put the promotion in an event, not on top of the attribute you will later group by.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Our review gate was the weakest model in the swarm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We had never benchmarked the critic, so we built one: a generated app with three documented contract defects, three runs per model, scored on defect recall. DeepSeek-V4-Pro, our incumbent primary, found 2 of 9. One run burned its entire 32768-token budget and returned nothing parseable. Kimi-K2.7-Code found 8 of 9 and was consistent across runs. The clearest pattern in the data was that on a review task, reasoning volume tracks defect recall: the terse models answered in under 250 output tokens and missed real bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A 20 percent pass rate was one missing sentence in the contract.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our plans specified field names and types but never formats, ranges or nullability. So the backend rejected &lt;code&gt;rating: 0&lt;/code&gt; while the frontend sent 0 as its default, and the backend demanded &lt;code&gt;YYYY-MM-DD&lt;/code&gt; while the frontend sent full ISO strings. Three consecutive generations hit the regeneration ceiling on exactly this class of disagreement. The fix was making the planner write a binding rules string per field, for example &lt;code&gt;"integer 0 to 5 inclusive, where 0 means unrated and is a valid value"&lt;/code&gt;. Both builders now read the same sentence. Catches dropped from 9 to 3 and the next run passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Our design system was making the output worse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We wrote a careful design guide so generated apps would not look like generated apps. Then we measured it: same model, same prompt, the only variable being whether the guide was attached. Without it, 14 inline SVGs, 3 animations and a deliberate typeface pairing. With it, 5 SVGs, 1 animation and Courier New. Three of our own rules did that. "System font stack is fine" told the model not to bother choosing type. "Cut any animation that does not serve the subject" read as licence to strip ornament. And our frontend prompt banned all external requests, which silently banned Google Fonts, so it could not have chosen a real typeface even if it wanted to. We had written a list of prohibitions, which is good at preventing bad output and bad at producing good output.&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%2Fe8ameuufkzpzemy0972q.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%2Fe8ameuufkzpzemy0972q.png" alt="Generated bookshelf app using system fonts and rainbow card colours" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before. Mono labels from a system stack, and card colours the backend invented at random.&lt;/em&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%2Fflnchfpeeg7stwtb4cj8.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%2Fflnchfpeeg7stwtb4cj8.png" alt="The same prompt generating a bookshelf app with Fraunces and a drawn SVG shelf" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After. Same model, same prompt, three rules removed from our guide: Fraunces display type, a drawn logo mark, filter chips carrying live counts, plus the books rendered as spines on a shelf.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There was a second layer to that one. The guide had a generous section for marketing sites, licensing scroll reveals, entrance sequences and layered depth, plus a stingy section for apps. Every app the swarm built was being held to a deliberately plainer standard than every site, and nobody had noticed because the sites looked great.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this was built
&lt;/h2&gt;

&lt;p&gt;DevSwarm was built with Claude Code, which is worth stating plainly rather than leaving as an inference. An AI coding agent helped build an AI coding agent, and the hackathon rules ask entrants to declare assistant use, so here it is.&lt;/p&gt;

&lt;p&gt;It is also relevant to the point of this post. Every finding in the section above started as a confident, wrong belief held by both of us, human and assistant alike. The stale token cap, the model we blamed for a provider's limit, the review gate we assumed was our strongest link, the design system we were sure was helping. None of those were resolved by reasoning harder about the code. They were resolved by a span event, a dashboard row or a benchmark disagreeing with us.&lt;/p&gt;

&lt;p&gt;That is the argument for instrumenting an agent system early. When you are building with agents, and with an agent, the telemetry is the only participant in the conversation with no opinion to defend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is worth copying from this build
&lt;/h2&gt;

&lt;p&gt;If you are instrumenting an agent system, three things paid for themselves immediately.&lt;/p&gt;

&lt;p&gt;Put the reason text in the span event. Not a code, not an enum, the actual provider error string. Both of our worst bugs were solved by reading that field, and neither would have been visible in a metric.&lt;/p&gt;

&lt;p&gt;Never overwrite an attribute you intend to group by. Add, do not replace.&lt;/p&gt;

&lt;p&gt;Make your product read its own telemetry. Our landing page statistics, our Doctor's diagnosis and our dashboards all run the same queries against the same trace store. It removes a whole category of drift, and it turns your observability stack from a debugging tool into a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The through line of every finding above is the same, and I have thought about it more than I expected to.&lt;/p&gt;

&lt;p&gt;Not one of these was a hard problem. A stale constant. A provider with a different limit. An attribute overwritten in the wrong place. A missing sentence in a contract. Three over-cautious lines in a style guide. Any of them would have been a five minute fix if we had known. Together they cost us most of a week and made the system look, from the outside, like the models were letting us down.&lt;/p&gt;

&lt;p&gt;They were not. Every single time, the model did exactly what our configuration told it to do. The failure was always upstream of the model, in something we had written and then stopped looking at.&lt;/p&gt;

&lt;p&gt;I think that is the actual lesson of building with agents, and it is not a comfortable one. The debugging skill is not prompt engineering. It is being willing to believe your instrumentation over your own memory of what you configured three days ago. We only got there because the telemetry kept producing numbers that made our explanations impossible.&lt;/p&gt;

&lt;p&gt;If you are building something similar, I would genuinely like to know whether your experience matches. My suspicion is that a lot of "the model is not good enough" is actually "my config is stale and I have no way to see it".&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DevSwarm: &lt;a href="https://github.com/himanshu748/devswarm" rel="noopener noreferrer"&gt;github.com/himanshu748/devswarm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;otel-swarm, the extracted instrumentation library: &lt;a href="https://github.com/himanshu748/otel-swarm" rel="noopener noreferrer"&gt;github.com/himanshu748/otel-swarm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz: &lt;a href="https://signoz.io" rel="noopener noreferrer"&gt;signoz.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The hackathon: &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz&lt;/a&gt; by WeMakeDevs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dashboards, alert rules and the Foundry &lt;code&gt;casting.yaml&lt;/code&gt; are all in the repo under &lt;code&gt;observability/&lt;/code&gt;, so the whole SigNoz side of this is reproducible rather than described.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The bug that never crashed: how I fuzzed an AI's own code sandbox and found it lying to its model</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:26:43 +0000</pubDate>
      <link>https://dev.to/himanshu_748/the-bug-that-never-crashed-how-i-fuzzed-an-ais-own-code-sandbox-and-found-it-lying-to-its-model-2ek2</link>
      <guid>https://dev.to/himanshu_748/the-bug-that-never-crashed-how-i-fuzzed-an-ais-own-code-sandbox-and-found-it-lying-to-its-model-2ek2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that never crashed
&lt;/h2&gt;

&lt;p&gt;The scariest bug I caught this month never threw a stack trace. It never paged anyone. It just quietly made an AI agent dumber, on repeat, and handed the blame to the model.&lt;/p&gt;

&lt;p&gt;Here is the whole story, because the way I found it turned out to matter more than any single fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it started
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/huggingface/smolagents" rel="noopener noreferrer"&gt;smolagents&lt;/a&gt; is Hugging Face's agent framework, 28k+ stars, and it has an unusual design: the agent &lt;em&gt;writes Python&lt;/em&gt; as its reasoning, and a sandboxed interpreter (&lt;code&gt;LocalPythonExecutor&lt;/code&gt;) runs that code. The model thinks by coding. So the sandbox is not a side feature. It is the surface the model lives on.&lt;/p&gt;

&lt;p&gt;I went in looking for one bug to fix for this challenge. Every obvious open issue was already claimed or had a competing PR. So instead of reading the issue tracker, I did something almost stupid: I fed the sandbox ordinary, boring, &lt;em&gt;valid&lt;/em&gt; Python and watched what it refused to run.&lt;/p&gt;

&lt;p&gt;The rule I gave myself: &lt;strong&gt;if it is valid Python and the sandbox rejects it, that is a bug.&lt;/strong&gt; The model writes valid Python. If the sandbox chokes on it, the agent pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first false rejection
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&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;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merging dicts with &lt;code&gt;**&lt;/code&gt;. Every LLM writes this. The sandbox's answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InterpreterError: NoneType is not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;None&lt;/code&gt; in that line. I stared at it, then went to the source. In Python's AST, a &lt;code&gt;**spread&lt;/code&gt; entry inside a dict literal has &lt;code&gt;None&lt;/code&gt; where the key would be, a marker that says "this is a spread." smolagents walked the keys and tried to &lt;em&gt;evaluate&lt;/em&gt; that &lt;code&gt;None&lt;/code&gt; as if it were an expression. So the spread marker got blamed on a value the developer never wrote.&lt;/p&gt;

&lt;p&gt;That is when it clicked. This was not a crash bug. It was a &lt;strong&gt;lying&lt;/strong&gt; bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why lying is worse than crashing
&lt;/h2&gt;

&lt;p&gt;Here is the part that turned a one-off fix into an obsession.&lt;/p&gt;

&lt;p&gt;The error was &lt;em&gt;handled&lt;/em&gt;. smolagents catches it and feeds it back to the model as guidance: "here is what went wrong, try again." Good design, normally. But the guidance was wrong. The message said &lt;code&gt;NoneType&lt;/code&gt;, and the model's code had no &lt;code&gt;None&lt;/code&gt;, so the model could not act on it. It did the only thing a faithful agent can do with a message that is already a lie: it retried the exact same valid code.&lt;/p&gt;

&lt;p&gt;And again. And again.&lt;/p&gt;

&lt;p&gt;I only &lt;em&gt;saw&lt;/em&gt; the loop because I had wired the demo to Sentry. One issue. Three events. Three identical failures on one line, each burning a real LLM call and a slot in the step budget, until the run gave up and produced a worse answer than it should have. Without something counting the events, you would never see the loop. You would just see an agent that "isn't very good," and you would go blame the model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A red stack trace is mercy. It tells you where to look. The polite, well-handled, misleading error is the one that eats your afternoon, or in this case, eats the agent's entire budget while looking like helpful feedback.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sentry's Seer read the same event and reached the same root cause I did, independently: &lt;code&gt;None&lt;/code&gt; keys in &lt;code&gt;ast.Dict&lt;/code&gt; fed to the evaluator, agent retries in a loop. That was the moment I trusted the pattern enough to go hunting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern held everywhere I looked
&lt;/h2&gt;

&lt;p&gt;Same fuzzer, same rule, more boring valid Python. The sandbox kept lying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;10 ** 10 ** 8&lt;/code&gt; did not error at all. It &lt;strong&gt;froze the entire process&lt;/strong&gt;, forever, and the execution timeout never fired, because a single big-integer operation runs as one uninterruptible C call that holds the GIL and never lets the timeout thread wake up. The faulthandler dump showed the main thread stuck before the timer even armed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;best, *rest = scores&lt;/code&gt; failed with "Cannot unpack tuple of wrong size." There was no wrong size. The sandbox simply never implemented starred unpacking, a feature Python shipped in 2008.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a, b = "hi"&lt;/code&gt; was rejected outright. Strings unpack fine in real Python.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[a, b] = [1, 2]&lt;/code&gt; silently assigned &lt;em&gt;nothing&lt;/em&gt;. No error, no values. The quietest failure of all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four different corners of the language. One personality: confidently wrong, politely delivered, and invisible unless you were counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes, briefly
&lt;/h2&gt;

&lt;p&gt;Each one came down to matching CPython instead of guessing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dict spread&lt;/strong&gt;: evaluate pairwise, a &lt;code&gt;None&lt;/code&gt; key means merge a mapping. And gate it on &lt;code&gt;hasattr(value, "keys")&lt;/code&gt;, not the &lt;code&gt;Mapping&lt;/code&gt; ABC, so duck-typed mappings work too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The freeze&lt;/strong&gt;: you cannot interrupt a GIL-holding C call, so refuse to start it. Estimate the result's bit length from the operands in O(1) and raise a real error above a cap, pointing the model at &lt;code&gt;pow(base, exp, mod)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpacking&lt;/strong&gt;: accept any iterable, support one starred target that absorbs the surplus into a list, handle list-pattern targets, and use CPython's exact error messages so that when the model &lt;em&gt;does&lt;/em&gt; pass the wrong count, it gets an actionable message instead of a dead end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four fixes, four PRs, sixty-plus new tests, all verified failing on &lt;code&gt;main&lt;/code&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist I did not expect
&lt;/h2&gt;

&lt;p&gt;I opened the PRs. Minutes later, OpenAI's Codex reviewer commented on two of them, and it was &lt;em&gt;right&lt;/em&gt; both times. On the big-int fix it caught that I had checked &lt;code&gt;type(x) is int&lt;/code&gt;, which lets &lt;code&gt;bool&lt;/code&gt; and int subclasses slip through the guard. On the dict fix it caught that my &lt;code&gt;Mapping&lt;/code&gt; ABC check was stricter than CPython.&lt;/p&gt;

&lt;p&gt;So the final scoreboard was: an AI wrote the buggy code, a different AI reviewed my AI-assisted fix and found the hole, and a third AI (Seer) had already confirmed the root cause. I was the one steering, but I spent a good chunk of this project as the human in a loop of machines checking each other. Fitting, for a bug about an agent that could not tell it was stuck.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fuzz the boundary with valid input.&lt;/strong&gt; Everyone fuzzes with garbage to find crashes. The higher-value bugs in an AI system are the &lt;em&gt;false rejections&lt;/em&gt;: valid input the system refuses, because that is what silently degrades a model that is doing everything right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A handled error is not a safe error.&lt;/strong&gt; If the message is wrong, "handled" just means the failure is quiet instead of loud. Quiet is worse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument for absence and repetition.&lt;/strong&gt; The tell for this whole bug class was not an exception. It was &lt;em&gt;the same event, three times.&lt;/em&gt; An agent getting a different error each step is exploring. An agent getting the same error from the same input is stuck, and the stuckness is invisible to the agent because the message looks like feedback. Count your events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bugs are fixed. The lesson I am keeping is simpler than any of them: the dangerous failures in an agent stack are not the loud crashes. They are the polite, confident, wrong messages that let the model fail on repeat while everyone blames the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/smolagents/pull/2553" rel="noopener noreferrer"&gt;PR: dict unpacking&lt;/a&gt; (&lt;a href="https://github.com/huggingface/smolagents/issues/2552" rel="noopener noreferrer"&gt;issue&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/smolagents/pull/2551" rel="noopener noreferrer"&gt;PR: the GIL-holding freeze&lt;/a&gt; (&lt;a href="https://github.com/huggingface/smolagents/issues/2473" rel="noopener noreferrer"&gt;issue&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/smolagents/pull/2556" rel="noopener noreferrer"&gt;PR: starred and iterable unpacking&lt;/a&gt; (&lt;a href="https://github.com/huggingface/smolagents/issues/2555" rel="noopener noreferrer"&gt;issue&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/smolagents/pull/2528" rel="noopener noreferrer"&gt;PR: MCP tool serialization&lt;/a&gt; (&lt;a href="https://github.com/huggingface/smolagents/issues/1108" rel="noopener noreferrer"&gt;issue&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one has its own full write-up with Sentry before/after evidence, if you want the deep dives. This was the story of how they all turned out to be the same bug wearing four different masks.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>The smolagents sandbox broke 'a, *b = list', one of Python's most common lines</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Tue, 21 Jul 2026 05:46:04 +0000</pubDate>
      <link>https://dev.to/himanshu_748/the-smolagents-sandbox-broke-a-b-list-one-of-pythons-most-common-lines-1fj3</link>
      <guid>https://dev.to/himanshu_748/the-smolagents-sandbox-broke-a-b-list-one-of-pythons-most-common-lines-1fj3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fourth entry, and the fuzzer that found &lt;a href="https://dev.to/himanshu_748/the-smolagents-bug-that-made-my-agent-retry-the-same-valid-code-three-times-2aka"&gt;entry 3&lt;/a&gt; is still paying out. Same failure family, different corner of the language: valid Python the sandbox refuses to run, with an error that lies about why.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-liner every developer writes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splitting a list into "the first one" and "the rest" is about as ordinary as Python gets. It is &lt;a href="https://peps.python.org/pep-3132/" rel="noopener noreferrer"&gt;PEP 3132&lt;/a&gt;, shipped in 2008. Under smolagents' sandbox it fails with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InterpreterError: Cannot unpack tuple of wrong size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no wrong size. &lt;code&gt;scores&lt;/code&gt; has four items and the pattern accepts any length of two or more. The message describes a problem that does not exist, so the agent does the only thing a faithful agent can do with a message that is already a lie: it retries the identical, valid code.&lt;/p&gt;

&lt;p&gt;And it is not just the starred form. All of these are standard Python and all of them were broken:&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;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# Cannot unpack tuple of wrong size
&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# Cannot unpack non-tuple value
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# silently assigns nothing at all
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;smolagents runs model-generated code in its own AST interpreter, and assignment targets go through one function, &lt;code&gt;set_value&lt;/code&gt;. It handled exactly one shape: a fixed-size &lt;code&gt;ast.Tuple&lt;/code&gt;.&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;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&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;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__iter__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot unpack non-tuple value&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot unpack tuple of wrong size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate holes hide in those few lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Starred targets.&lt;/strong&gt; &lt;code&gt;a, *b&lt;/code&gt; has two target elements but the value has three items, so &lt;code&gt;len(target.elts) != len(value)&lt;/code&gt; fires. The &lt;code&gt;*&lt;/code&gt; marker is never even looked at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strings and bytes.&lt;/strong&gt; They are explicitly excluded from the iterable path, so &lt;code&gt;a, b = "hi"&lt;/code&gt; is rejected even though CPython unpacks strings happily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List-pattern targets.&lt;/strong&gt; &lt;code&gt;[a, b] = ...&lt;/code&gt; is an &lt;code&gt;ast.List&lt;/code&gt;, not an &lt;code&gt;ast.Tuple&lt;/code&gt;, so it matches no branch, falls through, and silently assigns nothing. No error, no values, the worst kind of quiet.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Sentry angle: count the retries
&lt;/h2&gt;

&lt;p&gt;Same lesson as the last two entries, and Sentry keeps making it visible. The error is handled: the agent catches it and feeds it back to the model as guidance. But the guidance is wrong, so the model cannot act on it, so it loops. One bug, one misleading message, three identical failures burning three steps:&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%2Fu8mly77xnb0g86o1hxl4.jpg" 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%2Fu8mly77xnb0g86o1hxl4.jpg" alt="Sentry issue showing InterpreterError Cannot unpack tuple of wrong size, 3 events, environment before, transaction CodeAgent list split task" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three events on one issue is the retry loop made countable. Without it you would see a slow run, not a stuck one. Sentry's Seer read the same event and landed on the exact cause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;smolagents' custom Python interpreter does not support starred unpacking (e.g. &lt;code&gt;best, *rest = scores&lt;/code&gt;), treating it as a fixed-size tuple unpack. &lt;code&gt;set_value&lt;/code&gt; checks &lt;code&gt;len(target.elts) != len(value)&lt;/code&gt; and raises, without handling &lt;code&gt;ast.Starred&lt;/code&gt; targets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Rewrite the branch to match CPython instead of guessing:&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;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;elts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;
    &lt;span class="n"&gt;starred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&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;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Starred&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multiple starred expressions in assignment&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="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__iter__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cannot unpack non-iterable &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;starred&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;starred&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="n"&gt;n_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not enough values to unpack (expected at least &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n_after&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;split&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;n_after&lt;/span&gt;
        &lt;span class="c1"&gt;# assign head, then the starred target gets the middle as a list, then the tail
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;else&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not enough values to unpack (expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too many values to unpack (expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any iterable now unpacks, a single starred target absorbs the surplus into a list in any position (&lt;code&gt;a, *b&lt;/code&gt;, &lt;code&gt;*a, b&lt;/code&gt;, &lt;code&gt;a, *b, c&lt;/code&gt;), list-pattern targets work, and the size errors read exactly like CPython's, so when the model genuinely does pass the wrong number of values it gets an actionable message instead of a dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  After
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app: step 1 ok, output = (90, [82, 71, 65])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One step. No loop. &lt;code&gt;best&lt;/code&gt; is 90, &lt;code&gt;rest&lt;/code&gt; is the tail, the way the model expected all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;4 valid unpacking forms fixed: starred targets, string unpacking, list-pattern targets, and the CPython error messages&lt;/li&gt;
&lt;li&gt;Reproduced on current &lt;code&gt;main&lt;/code&gt; and 1.26.0; 3 wasted agent steps per occurrence, visible only because Sentry counts events&lt;/li&gt;
&lt;li&gt;14 new tests plus one existing test updated to the improved message&lt;/li&gt;
&lt;li&gt;411 passing, ruff clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/huggingface/smolagents/issues/2555" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/issues/2555&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/huggingface/smolagents/pull/2556" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/pull/2556&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The through-line across all four entries has not changed: the dangerous agent bugs are not the loud crashes, they are the polite, well-handled, wrong messages that let the model fail on repeat. Fuzz the sandbox with ordinary valid code, and count your events.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>The smolagents bug that made my agent retry the same valid code three times</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:19:13 +0000</pubDate>
      <link>https://dev.to/himanshu_748/the-smolagents-bug-that-made-my-agent-retry-the-same-valid-code-three-times-2aka</link>
      <guid>https://dev.to/himanshu_748/the-smolagents-bug-that-made-my-agent-retry-the-same-valid-code-three-times-2aka</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Third entry in the DEV x Sentry Bug Smash. &lt;a href="https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im"&gt;Entry 1&lt;/a&gt; was a crash with a confusing message. &lt;a href="https://dev.to/himanshu_748/one-line-of-math-froze-my-ai-agent-forever-the-timeout-watched-and-did-nothing-2dma"&gt;Entry 2&lt;/a&gt; was a freeze the timeout could not catch. This one is quieter and sneakier: valid Python that the sandbox rejects with an error pointing at the wrong thing entirely.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When the open issues run out, fuzz
&lt;/h2&gt;

&lt;p&gt;By entry 3 every obvious open smolagents bug was already claimed or had a competing PR. So instead of reading the issue tracker I pointed a small fuzzer at the piece of smolagents that runs the most untrusted code: &lt;code&gt;LocalPythonExecutor&lt;/code&gt;, the sandbox that executes model-generated Python.&lt;/p&gt;

&lt;p&gt;The method is boring and effective: feed it ordinary, valid Python one snippet at a time, and flag anything that raises &lt;code&gt;InterpreterError&lt;/code&gt;. Valid Python that the sandbox refuses to run is, by definition, a bug, because the model writes valid Python and expects it to work.&lt;/p&gt;

&lt;p&gt;That surfaced four unreported bugs in one afternoon. This post is about the one I shipped: &lt;strong&gt;dict unpacking&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&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;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merging dicts with &lt;code&gt;**&lt;/code&gt; is one of the most common things an LLM writes. Under smolagents it fails with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InterpreterError: NoneType is not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;None&lt;/code&gt; anywhere in that line. The message sends you looking for a null value that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;In Python's AST, a dict literal keeps its keys and values in two parallel lists. For a normal entry the key is an AST node. For a &lt;code&gt;**mapping&lt;/code&gt; spread entry, the key is literally &lt;code&gt;None&lt;/code&gt;, a signal that says "this is a spread, not a key/value pair."&lt;/p&gt;

&lt;p&gt;smolagents evaluated every key by walking &lt;code&gt;expression.keys&lt;/code&gt; and calling &lt;code&gt;evaluate_ast(key, ...)&lt;/code&gt; on each one. When the key is &lt;code&gt;None&lt;/code&gt;, that call falls through every &lt;code&gt;isinstance&lt;/code&gt; branch to the catch-all &lt;code&gt;raise InterpreterError(f"{type} is not supported")&lt;/code&gt;. So the spread marker got evaluated as if it were an expression, and the model got blamed for a &lt;code&gt;None&lt;/code&gt; it never wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why silence is the expensive part
&lt;/h2&gt;

&lt;p&gt;Here is the part the Sentry view made obvious. The error is &lt;em&gt;handled&lt;/em&gt;: the agent catches it and feeds it back to the model as "here is what went wrong, try again." But the message names &lt;code&gt;NoneType&lt;/code&gt;, and the model's code has no &lt;code&gt;None&lt;/code&gt;, so the model cannot act on it. It retries the exact same valid syntax. And again. Every step burns a real LLM call and a slot in the step budget until the run gives up.&lt;/p&gt;

&lt;p&gt;One bug, one misleading message, three identical failures:&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%2Fduexgfbm8suc843xzgvv.jpg" 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%2Fduexgfbm8suc843xzgvv.jpg" alt="Sentry issue showing InterpreterError NoneType is not supported, 3 events, environment before, transaction CodeAgent config merge task" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three events on a single issue is not noise. It is the agent stuck in a loop, and without Sentry counting the events you would never see the loop, only a run that quietly underperformed. Sentry's Seer read the same event and reached the exact root cause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;smolagents' LocalPythonExecutor doesn't handle dict unpacking (&lt;code&gt;**&lt;/code&gt;) syntax: None keys in ast.Dict cause an unsupported type error. [...] &lt;code&gt;evaluate_ast(None, ...)&lt;/code&gt; matches no isinstance branch and falls to the else clause. The interpreter raises InterpreterError: NoneType is not supported, the agent retries with identical code, burning steps in a loop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Evaluate the dict pairwise instead of evaluating keys blindly. A &lt;code&gt;None&lt;/code&gt; key means "merge this mapping":&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;result&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;key_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&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;key_node&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_ast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;common_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InterpreterError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; object is not a mapping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&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="nf"&gt;evaluate_ast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;common_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_ast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;common_params&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches CPython exactly: spreads merge in order, later keys win, and unpacking a non-mapping raises &lt;code&gt;'list' object is not a mapping&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reviewer caught my fix being too strict
&lt;/h2&gt;

&lt;p&gt;I first gated the spread on &lt;code&gt;isinstance(value, Mapping)&lt;/code&gt;. Minutes after the PR opened, OpenAI's Codex reviewer flagged it (P2): CPython does not require the &lt;code&gt;Mapping&lt;/code&gt; ABC, it only requires an object with a &lt;code&gt;keys()&lt;/code&gt; method. Since the sandbox lets users define their own classes, a duck-typed mapping with &lt;code&gt;keys()&lt;/code&gt; and &lt;code&gt;__getitem__()&lt;/code&gt; would have been wrongly rejected. I switched the check to &lt;code&gt;hasattr(value, "keys")&lt;/code&gt; and added a test for exactly that case. AI wrote the code, AI reviewed the code, I kept score.&lt;/p&gt;

&lt;h2&gt;
  
  
  After
&lt;/h2&gt;

&lt;p&gt;On the patched build the same line just runs:&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;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&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;temperature&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_p&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One step, no loop, no phantom &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;4 unreported bugs found by fuzzing valid Python through the sandbox; this is the first fix&lt;/li&gt;
&lt;li&gt;Misleading &lt;code&gt;NoneType&lt;/code&gt; error reproduced on current &lt;code&gt;main&lt;/code&gt; and 1.26.0&lt;/li&gt;
&lt;li&gt;3 wasted agent steps per occurrence, visible only because Sentry counts events&lt;/li&gt;
&lt;li&gt;9 new tests: spreads, double spreads, override order both ways, a duck-typed mapping class, empty spread, non-mapping rejection&lt;/li&gt;
&lt;li&gt;406 passing, ruff clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/huggingface/smolagents/issues/2552" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/issues/2552&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/huggingface/smolagents/pull/2553" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/pull/2553&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern across all three entries: the worst agent bugs do not throw a red stack trace at you. They hand the model a plausible-but-wrong message and let it fail politely, on repeat. Count your events.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>One line of math froze my AI agent forever. The timeout watched and did nothing.</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sun, 19 Jul 2026 07:54:58 +0000</pubDate>
      <link>https://dev.to/himanshu_748/one-line-of-math-froze-my-ai-agent-forever-the-timeout-watched-and-did-nothing-2dma</link>
      <guid>https://dev.to/himanshu_748/one-line-of-math-froze-my-ai-agent-forever-the-timeout-watched-and-did-nothing-2dma</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is my second entry for the DEV x Sentry Bug Smash challenge. &lt;a href="https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im"&gt;Entry #1 was a crash with a confusing error message&lt;/a&gt;. This one is the opposite and it is scarier: no crash, no message, no event. Just silence.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that sends you nothing
&lt;/h2&gt;

&lt;p&gt;smolagents runs LLM-generated Python in a sandboxed executor with a timeout. Issue &lt;a href="https://github.com/huggingface/smolagents/issues/2473" rel="noopener noreferrer"&gt;#2473&lt;/a&gt; claims that one line of model-generated math defeats it completely:&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;smolagents.local_python_executor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LocalPythonExecutor&lt;/span&gt;

&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalPythonExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additional_authorized_imports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_tools&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
&lt;span class="nf"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10 ** 10 ** 8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 2 second timeout. Should be fine, right?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this with a 2 second timeout and a faulthandler bomb set for 20 seconds. The timeout never fired. The process sat frozen until the external kill. An agent that generates this expression (and "compute this huge number" is exactly the kind of thing agents try) freezes its host process forever.&lt;/p&gt;

&lt;p&gt;Zero comments on the issue, zero PRs. Mine now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the timeout lies to you
&lt;/h2&gt;

&lt;p&gt;smolagents' timeout is thread based: a worker thread runs the code, the main thread waits in &lt;code&gt;future.result(timeout=2)&lt;/code&gt;. That design is fine for almost everything, because CPython switches threads between bytecode instructions.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;10 ** 10 ** 8&lt;/code&gt; is not "almost everything". CPython computes arbitrary precision &lt;code&gt;**&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt; inside a single C call that holds the GIL from start to finish. No bytecode boundary, no thread switch, no timeout. The result would have about 400 million bits. The computation takes somewhere between minutes and hours. Your watchdog needs the GIL to wake up, and it never gets it.&lt;/p&gt;

&lt;p&gt;The faulthandler dump made it concrete, and it was worse than the issue described:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 0x16d1f3000 (worker):
  File "local_python_executor.py", line 753 in evaluate_binop   &amp;lt;- computing the pow

Thread 0x1f00d5e80 (main):
  File "threading.py", line 999 in start
  File "concurrent/futures/thread.py", line 180 in submit       &amp;lt;- never returned!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main thread was still stuck inside &lt;code&gt;ThreadPoolExecutor.submit&lt;/code&gt;. It never even reached &lt;code&gt;future.result&lt;/code&gt;. The 2 second timer never armed at all.&lt;/p&gt;

&lt;p&gt;The existing &lt;code&gt;MAX_OPERATIONS&lt;/code&gt; guard (10 million AST operations) does not help either. This is a handful of AST nodes. The entire cost lives inside one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sentry angle: monitoring for absence
&lt;/h2&gt;

&lt;p&gt;Entry #1 was about a noisy failure. This bug is the opposite. I pointed a fresh Sentry project at a simulated agent run on the unpatched PyPI release (1.26.0) and got the most unsettling result possible: nothing. No event, no transaction, an empty project. The process was frozen mid-transaction and the SDK never got a chance to flush.&lt;/p&gt;

&lt;p&gt;The lesson: for freeze-class bugs you need a supervisor. I added a small watchdog process that gives the worker 25 seconds, then kills it and reports what it saw, with the worker's faulthandler stack attached as evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;watchdog: starting worker (before) with 25s budget
worker: smolagents 1.26.0
worker: step 1 executing 'result = 10 ** 10 ** 8'...
watchdog: worker FROZE, killed it, reporting to Sentry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting Sentry issue carries the whole story in one place: environment tagged &lt;code&gt;before&lt;/code&gt;, handled by the supervisor, with the frozen frame (&lt;code&gt;evaluate_binop&lt;/code&gt;, line 753 of &lt;code&gt;local_python_executor.py&lt;/code&gt;) sitting in the attached &lt;code&gt;worker_faulthandler_stack&lt;/code&gt; extra.&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%2Fsz43j2346qhu7l8dpxkd.jpg" 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%2Fsz43j2346qhu7l8dpxkd.jpg" alt="Sentry issue AgentFrozenError, resolved, environment before, reported by the watchdog after the worker froze" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sentry's Seer ran root cause analysis on that issue and independently landed on the same conclusion: signal and thread interruption need the GIL, and a single C-level big-int operation never releases it.&lt;/p&gt;

&lt;p&gt;Seer's verdict, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CodeAgent's 2s timeout uses Python signal-based interruption, which cannot fire during uninterruptible C-level big-int operations that hold the GIL. [...] A single large big-integer arithmetic operation runs entirely as one C-level call that holds the GIL continuously without yielding. CPython cannot deliver signals or switch threads during an uninterruptible C extension call, so no timeout callback fires for the duration of that operation.&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%2Fsdf9o9kktuixsbba525w.jpg" 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%2Fsdf9o9kktuixsbba525w.jpg" alt="Sentry Seer Autofix root cause panel identifying the GIL-holding big-int operation as the reason the timeout never fired" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: you cannot interrupt it, so refuse to start it
&lt;/h2&gt;

&lt;p&gt;Killing the computation mid-flight is impossible from Python. But predicting the damage is O(1). Before executing &lt;code&gt;**&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt; on integers, the executor now estimates the result's bit length from the operands' bit lengths:&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;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;estimated_bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bit_length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;       &lt;span class="c1"&gt;# upper bound
&lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;estimated_bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bit_length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;estimated_bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bit_length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bit_length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above 1 million bits (about 300k digits, still generous) it raises an informative &lt;code&gt;InterpreterError&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;Operation '**' would produce an integer of around 400000000 bits, exceeding
the maximum of 1000000 bits allowed. Use smaller operands, or
pow(base, exp, mod) for modular exponentiation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message matters. The agent surfaces it to the model, and the model can actually act on it: use &lt;code&gt;pow(base, exp, mod)&lt;/code&gt;, which stays unrestricted because modular exponentiation is fast and legitimate. The agent recovers on the next step instead of hanging the host.&lt;/p&gt;

&lt;p&gt;The after run on the patched build: the guard rejects the expression in 0.0 seconds, the error lands in Sentry as a normal actionable issue and step 2 executes fine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;watchdog: starting worker (after) with 25s budget
worker: smolagents 1.27.0.dev0
worker: step 1 error surfaced to the model: InterpreterError: ...
worker: step 2 executing '2 + 2'...
worker: step 2 ok
worker: DONE
watchdog: worker exited with code 0
&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%2Fyyx3pwllkkuno7gx6leg.jpg" 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%2Fyyx3pwllkkuno7gx6leg.jpg" alt="Sentry issue showing the informative InterpreterError captured on the patched build, environment after, transaction CodeAgent math task, resolved" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A robot reviewed my robot fix
&lt;/h2&gt;

&lt;p&gt;Minutes after I opened the PR, OpenAI's Codex reviewer flagged a real hole: my guard checked &lt;code&gt;type(x) is int&lt;/code&gt;, which lets &lt;code&gt;bool&lt;/code&gt; and &lt;code&gt;int&lt;/code&gt; subclasses slip through. &lt;code&gt;True &amp;lt;&amp;lt; 10**9&lt;/code&gt; and &lt;code&gt;class BigInt(int)&lt;/code&gt; still reached the uninterruptible C calls. Fixed with &lt;code&gt;isinstance&lt;/code&gt;, added both as regression tests. AI found the bug class, AI fixed it, AI reviewed the fix. I just steered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Freeze reproduced at 20+ seconds (would have run for hours), external kill required&lt;/li&gt;
&lt;li&gt;Fix rejects the same expression in 0.0 seconds&lt;/li&gt;
&lt;li&gt;9 explosive patterns blocked: &lt;code&gt;**&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;, chained &lt;code&gt;*&lt;/code&gt;, all augmented forms, &lt;code&gt;pow(a, b)&lt;/code&gt;, bool and int subclass variants&lt;/li&gt;
&lt;li&gt;10 legitimate operations verified untouched: 100! via repeated &lt;code&gt;*=&lt;/code&gt;, &lt;code&gt;pow(7, 2**64, 97)&lt;/code&gt;, float pow, &lt;code&gt;1 ** 10**9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;19 new tests, full test file 416 passed, ruff clean&lt;/li&gt;
&lt;li&gt;The blocked-pattern tests hang forever on unpatched main. I verified that the honest way, with a stash and a kill switch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/huggingface/smolagents/issues/2473" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/issues/2473&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/huggingface/smolagents/pull/2551" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/pull/2551&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Entry #1: &lt;a href="https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im"&gt;https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scariest bugs are not the ones that page you at 3am. They are the ones that make sure nothing ever pages you at all. Instrument for silence.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>I fixed a smolagents bug that confused everyone who hit it (with Sentry watching the whole time)</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Thu, 16 Jul 2026 05:31:48 +0000</pubDate>
      <link>https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im</link>
      <guid>https://dev.to/himanshu_748/i-fixed-a-smolagents-bug-that-confused-everyone-who-hit-it-with-sentry-watching-the-whole-time-1im</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;I picked &lt;a href="https://github.com/huggingface/smolagents" rel="noopener noreferrer"&gt;huggingface/smolagents&lt;/a&gt;, the 28k+ star agent framework where agents literally think in Python code. It had a bug open since &lt;a href="https://github.com/huggingface/smolagents/issues/1108" rel="noopener noreferrer"&gt;issue #1108&lt;/a&gt; that anyone combining MCP tools with agent serialization would eventually slam into.&lt;/p&gt;

&lt;p&gt;Call &lt;code&gt;agent.to_dict()&lt;/code&gt; on a CodeAgent holding MCP tools (or &lt;code&gt;save()&lt;/code&gt; or &lt;code&gt;push_to_hub()&lt;/code&gt;, same path) and you get this beauty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: Tool validation failed for MCPAdaptTool:
Parameters in __init__ must have default values, found required parameters: name, description, inputs, output_type
- forward: Name 'func' is undefined.
- forward: Name 'mcp' is undefined.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;func&lt;/code&gt; is undefined? &lt;code&gt;mcp&lt;/code&gt; is undefined? I never wrote a &lt;code&gt;forward&lt;/code&gt; method. If you hit this in the wild you'd have zero idea what you did wrong. Spoiler: you did nothing wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;smolagents serializes tools by reconstructing standalone Python source for the tool class. &lt;code&gt;Tool.to_dict&lt;/code&gt; calls &lt;code&gt;validate_tool_attributes()&lt;/code&gt; which does static AST analysis, then &lt;code&gt;instance_to_source()&lt;/code&gt; so &lt;code&gt;Tool.from_code&lt;/code&gt; can rebuild the tool later from source alone.&lt;/p&gt;

&lt;p&gt;That contract can never hold for MCP tools. &lt;code&gt;MCPAdaptTool&lt;/code&gt; is generated at runtime by mcpadapt inside a closure. Its &lt;code&gt;__init__&lt;/code&gt; takes required parameters and its &lt;code&gt;forward&lt;/code&gt; closes over the live MCP client session (&lt;code&gt;func&lt;/code&gt;, &lt;code&gt;mcp&lt;/code&gt;, &lt;code&gt;logger&lt;/code&gt;...). The tool's actual behavior lives on the MCP server, not in Python source. There is no source to reconstruct, so the AST validator chokes on a class that was never meant to pass it.&lt;/p&gt;

&lt;p&gt;The interesting part: &lt;code&gt;Tool.to_dict&lt;/code&gt; already fails fast with a clear message for three other runtime generated wrappers (Spaces, LangChain, Gradio). MCP tools were just missing from that guard.&lt;/p&gt;

&lt;p&gt;Sometimes the right fix isn't making the impossible possible, it's failing loudly and helpfully. Recreating a live MCP session from serialized state would mean silently re-establishing server connections with credentials and trust decisions the library has no business making. So I extended the existing guard to detect MCP tools and raise this instead:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: Cannot serialize MCP tool 'echo_tool': it wraps a live MCP server session, which cannot be
saved as standalone code. Remove MCP tools from your agent before calling to_dict, save or push_to_hub,
and recreate them with MCPClient or ToolCollection.from_mcp when loading the agent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;From "what is func" to "here's exactly what to do instead" in one guard clause.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/huggingface/smolagents/pull/2528" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Raise informative error when serializing MCP tools
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#2528&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/himanshu748" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F77563702%3Fv%3D4" alt="himanshu748 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/himanshu748" rel="noopener noreferrer"&gt;himanshu748&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/huggingface/smolagents/pull/2528" rel="noopener noreferrer"&gt;&lt;time&gt;Jul 14, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;Fixes #1108&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Problem&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Calling &lt;code&gt;to_dict()&lt;/code&gt; (and therefore &lt;code&gt;save()&lt;/code&gt; or &lt;code&gt;push_to_hub()&lt;/code&gt;) on an agent that holds MCP tools crashes with a confusing internal error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ValueError: Tool validation failed for MCPAdaptTool:
Parameters in __init__ must have default values, found required parameters: name, description, inputs, output_type
- forward: Name 'func' is undefined.
- forward: Name 'mcp' is undefined.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reproduction (stdio MCP server, same shape as the tests in &lt;code&gt;tests/test_mcp_client.py&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight highlight-source-python js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;mcp&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;StdioServerParameters&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;smolagents&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;CodeAgent&lt;/span&gt;, &lt;span class="pl-v"&gt;InferenceClientModel&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;smolagents&lt;/span&gt;.&lt;span class="pl-s1"&gt;mcp_client&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;MCPClient&lt;/span&gt;

&lt;span class="pl-s1"&gt;server_parameters&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;StdioServerParameters&lt;/span&gt;(&lt;span class="pl-s1"&gt;command&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"python"&lt;/span&gt;, &lt;span class="pl-s1"&gt;args&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"-c"&lt;/span&gt;, &lt;span class="pl-s1"&gt;echo_server_script&lt;/span&gt;])
&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;MCPClient&lt;/span&gt;(&lt;span class="pl-s1"&gt;server_parameters&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;tools&lt;/span&gt;:
    &lt;span class="pl-s1"&gt;agent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;CodeAgent&lt;/span&gt;(&lt;span class="pl-s1"&gt;model&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-en"&gt;InferenceClientModel&lt;/span&gt;(), &lt;span class="pl-s1"&gt;tools&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-en"&gt;list&lt;/span&gt;(&lt;span class="pl-s1"&gt;tools&lt;/span&gt;))
    &lt;span class="pl-s1"&gt;agent&lt;/span&gt;.&lt;span class="pl-c1"&gt;to_dict&lt;/span&gt;()  &lt;span class="pl-c"&gt;# ValueError: Tool validation failed for MCPAdaptTool: ...&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Root cause&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Tool.to_dict&lt;/code&gt; serializes a tool by reconstructing standalone source code for its class: it calls &lt;code&gt;validate_tool_attributes(self.__class__)&lt;/code&gt; and &lt;code&gt;instance_to_source(...)&lt;/code&gt; so that &lt;code&gt;Tool.from_code&lt;/code&gt; can later rebuild the tool from that source alone.&lt;/p&gt;
&lt;p&gt;That contract cannot hold for MCP tools. &lt;code&gt;MCPAdaptTool&lt;/code&gt; is generated at runtime by &lt;code&gt;mcpadapt&lt;/code&gt;, its &lt;code&gt;__init__&lt;/code&gt; takes required parameters and its &lt;code&gt;forward&lt;/code&gt; is a closure over the live MCP client session (&lt;code&gt;func&lt;/code&gt;, &lt;code&gt;mcp&lt;/code&gt;, &lt;code&gt;logger&lt;/code&gt;, ...). The tool's behavior lives on the MCP server, not in Python source, and the underlying connection is not serializable, so source reconstruction fails validation with the cryptic error above.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Tool.to_dict&lt;/code&gt; already fails fast with a clear message for the other three runtime generated wrapper classes (&lt;code&gt;SpaceToolWrapper&lt;/code&gt;, &lt;code&gt;LangChainToolWrapper&lt;/code&gt;, &lt;code&gt;GradioToolWrapper&lt;/code&gt;). MCP tools were missing from that guard.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Fix&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Extend the existing guard in &lt;code&gt;Tool.to_dict&lt;/code&gt; to detect MCP tools and raise an actionable error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ValueError: Cannot serialize MCP tool 'echo_tool': it wraps a live MCP server session, which cannot be
saved as standalone code. Remove MCP tools from your agent before calling to_dict, save or push_to_hub,
and recreate them with MCPClient or ToolCollection.from_mcp when loading the agent.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Detection matches the runtime class name, following the existing convention in the same block, since &lt;code&gt;mcpadapt&lt;/code&gt; is an optional dependency. The &lt;code&gt;from_dict&lt;/code&gt; direction needs no change: serialization now fails fast with a clear message, and recreating a live MCP session is a user decision (server lifecycle, credentials, trust) that &lt;code&gt;Tool.from_code&lt;/code&gt; could never perform safely.&lt;/p&gt;
&lt;p&gt;A note documenting the limitation is added to the MCP section of the tools tutorial.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tests&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Two tests in &lt;code&gt;tests/test_mcp_client.py&lt;/code&gt;, using the existing &lt;code&gt;echo_server_script&lt;/code&gt; stdio fixture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;test_mcp_tool_to_dict_raises_informative_error&lt;/code&gt;: &lt;code&gt;tool.to_dict()&lt;/code&gt; raises the clear error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_agent_to_dict_with_mcp_tool_raises_informative_error&lt;/code&gt;: &lt;code&gt;CodeAgent.to_dict()&lt;/code&gt; raises the clear error (the exact scenario from the issue).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both fail on &lt;code&gt;main&lt;/code&gt; with the old &lt;code&gt;Tool validation failed for MCPAdaptTool&lt;/code&gt; error and pass with this change. &lt;code&gt;make quality&lt;/code&gt; passes. &lt;code&gt;tests/test_mcp_client.py&lt;/code&gt; (7 passed), &lt;code&gt;tests/test_tools.py&lt;/code&gt; and the agent serialization tests in &lt;code&gt;tests/test_agents.py&lt;/code&gt; pass locally; the two pre-existing failures in &lt;code&gt;test_integration_from_mcp_with_streamable_http&lt;/code&gt; and &lt;code&gt;test_integration_from_mcp_with_sse&lt;/code&gt; also fail on a clean &lt;code&gt;main&lt;/code&gt; checkout (local port binding) and are unrelated.&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/huggingface/smolagents/pull/2528" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;PR: &lt;a href="https://github.com/huggingface/smolagents/pull/2528" rel="noopener noreferrer"&gt;https://github.com/huggingface/smolagents/pull/2528&lt;/a&gt; (Fixes #1108)&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%2Favbbb6xwpf3wwx61e879.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%2Favbbb6xwpf3wwx61e879.png" alt="PR #2528 on GitHub" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two new tests using the existing stdio echo server fixture, both fail on main and pass with the fix. &lt;code&gt;make quality&lt;/code&gt; clean. Docs note added to the MCP tools tutorial so nobody has to learn this the hard way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Users hitting this now get an actionable error instead of AST validator internals&lt;/li&gt;
&lt;li&gt;The fix follows the repo's existing convention exactly (same guard block, same style as the Space/LangChain/Gradio cases), which is what makes a one-commit PR actually mergeable&lt;/li&gt;
&lt;li&gt;Documented the limitation where users would look for it&lt;/li&gt;
&lt;li&gt;Regression tests covering both the raw tool and the full CodeAgent scenario from the original issue&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;This is where it gets fun. I built a small demo agent app (a weather checkpoint agent using my patched smolagents with an MCP tool) and wired in the Sentry Python SDK with error monitoring, tracing and AI agent monitoring before touching the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: catch the crash.&lt;/strong&gt; Running the demo on unpatched smolagents, the cryptic ValueError landed straight in Sentry as an unhandled issue with the full 20+ line "undefined name" spam captured.&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%2Fgdvqbayupa9s1hgzx56k.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%2Fgdvqbayupa9s1hgzx56k.png" alt="Sentry issue: the cryptic ValueError captured, now marked resolved" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: let Seer take a shot.&lt;/strong&gt; I ran Seer root cause analysis on the captured issue. Its diagnosis, fully independent of my PR:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MCPAdaptTool is a dynamically generated inner class created by mcpadapt's SmolAgentsAdapter.adapt() closure, so its &lt;strong&gt;init&lt;/strong&gt; has required parameters and its methods reference closure variables that are not visible as class-level attributes... making it fundamentally incompatible with smolagents' static source-code validation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is, almost line for line, the root cause I wrote in the PR. AI-assisted debugging where the AI and the human converge on the same diagnosis independently is exactly the confidence check you want before shipping a fix upstream.&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%2Flliddoqvtunwmbtac1u3.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%2Flliddoqvtunwmbtac1u3.png" alt="Seer root cause analysis of the MCPAdaptTool validation failure" width="800" height="444"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Step 3: verify and resolve.&lt;/strong&gt; Same demo on the patched version runs clean, agent traces show the gen_ai spans (invoke_agent, execute_tool) nested under the workflow and the Sentry issue is marked resolved.&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%2Fwv9oosy8uweowcm25dj8.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%2Fwv9oosy8uweowcm25dj8.png" alt="Agent trace with gen_ai spans: invoke_agent, checkpoint_agent and execute_tool" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Used the &lt;code&gt;bugsmash26&lt;/code&gt; code for the $100 credits too. Thanks Sentry 🛹&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Serialization boundaries are where abstractions leak. smolagents' "tools are source code" model is elegant right up until a tool is actually a live network session wearing a Tool costume. The mature move for a library isn't to pretend otherwise, it's to name the limitation clearly at the exact moment the user hits it.&lt;/p&gt;

&lt;p&gt;Also: watching Seer independently arrive at your root cause is a genuinely great feeling. Like a second engineer nodding at your RCA.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built during DEV's first Summer Bug Smash. Find me on GitHub &lt;a href="https://github.com/himanshu748" rel="noopener noreferrer"&gt;@himanshu748&lt;/a&gt; or X &lt;a href="https://x.com/jhahimanshu653" rel="noopener noreferrer"&gt;@jhahimanshu653&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Traced a Multi-Step LLM Agent With Self-Hosted SigNoz. One Feature Sold Me.</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 11 Jul 2026 08:22:06 +0000</pubDate>
      <link>https://dev.to/himanshu_748/i-traced-a-multi-step-llm-agent-with-self-hosted-signoz-one-feature-sold-me-4k71</link>
      <guid>https://dev.to/himanshu_748/i-traced-a-multi-step-llm-agent-with-self-hosted-signoz-one-feature-sold-me-4k71</guid>
      <description>&lt;p&gt;Multi-step LLM agents fail in a way normal backends don't. Nothing crashes. The pipeline "works", the answer is just bad, slow or three times more expensive than yesterday. &lt;code&gt;print()&lt;/code&gt; debugging tells you nothing, because the interesting question is never "did step 3 run". It is "what did step 3 see, which model actually answered and what did it cost".&lt;/p&gt;

&lt;p&gt;So I self-hosted SigNoz and pointed a simulated agent pipeline at it: a four-step research assistant (plan, retrieve, generate, synthesize) instrumented with OpenTelemetry, emitting traces, metrics and logs, with GenAI semantic-convention attributes (&lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt; and friends) on every LLM call.&lt;/p&gt;

&lt;p&gt;This post is about the feature that turned out to be the most useful. It was not the one I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: one CLI, eight lines of YAML
&lt;/h2&gt;

&lt;p&gt;Self-hosting used to mean wrangling a long docker-compose file. SigNoz now ships &lt;strong&gt;Foundry&lt;/strong&gt;, a small CLI that casts the whole stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash   &lt;span class="c"&gt;# installs foundryctl (checksum-verified)&lt;/span&gt;
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml                  &lt;span class="c"&gt;# deploys the full stack on Docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a &lt;code&gt;casting.yaml&lt;/code&gt; that is all of eight lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Installation&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;flavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few minutes of image pulls later, ClickHouse, Postgres, the SigNoz backend and an OTel collector were running. The UI is on &lt;code&gt;localhost:8080&lt;/code&gt; and the collector listens on &lt;code&gt;4317&lt;/code&gt; (gRPC) and &lt;code&gt;4318&lt;/code&gt; (HTTP).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One gotcha worth knowing:&lt;/strong&gt; telemetry is rejected until you create the admin account in the UI. The collector registers with the backend over OpAMP, and until an organization exists the backend answers "cannot create agent without orgId" and the OTLP ports reset every connection. If your exporter logs &lt;code&gt;Connection reset by peer&lt;/code&gt; on a fresh install, you haven't finished the two-minute signup at &lt;code&gt;localhost:8080&lt;/code&gt; yet. Create the account and ingestion starts working within about thirty seconds, no restarts needed.&lt;/p&gt;

&lt;p&gt;My demo app needed zero SigNoz-specific code: the stock OpenTelemetry SDK exporting OTLP to &lt;code&gt;localhost:4318&lt;/code&gt;. That is the point of an OTel-native backend. There is no vendor agent, so nothing about the app knows SigNoz exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline being observed
&lt;/h2&gt;

&lt;p&gt;Each simulated request produces one trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent.request                    (root, 2.02s)
├── agent.plan                   (850ms)
│   └── gen_ai.generate plan     (696ms, gen_ai.* attributes)
├── agent.retrieve               (461ms, sometimes errors: vector store timeout)
├── agent.generate               (564ms)
│   └── gen_ai.generate answer   (564ms, gen_ai.* attributes)
└── agent.synthesize             (139ms, sometimes errors: citation validation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;gen_ai.generate&lt;/code&gt; span carries the GenAI semantic conventions: operation name, provider, model, input tokens and output tokens. Alongside the traces the app emits counters for token usage (&lt;code&gt;gen_ai.client.token.usage&lt;/code&gt;) and estimated spend (&lt;code&gt;agent.llm.cost&lt;/code&gt;), both tagged by model and provider, a request-duration histogram and structured logs that inherit the active trace context automatically.&lt;/p&gt;

&lt;p&gt;Sixty simulated requests later: 420 spans, 134 logs and a few hundred metric samples, all visible in the UI. The Services page picked up &lt;code&gt;research-assistant&lt;/code&gt; on its own with RED metrics already computed (p99 latency, error rate, throughput). I wrote no configuration for that.&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%2Feqko3iqz9abfr3myg78p.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%2Feqko3iqz9abfr3myg78p.png" alt="Trace waterfall of one agent request: plan, retrieve, generate and synthesize with the LLM calls nested inside" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I expected to love: the trace waterfall
&lt;/h2&gt;

&lt;p&gt;And it is genuinely good. The trace detail view renders a flame graph and waterfall that read exactly like the agent's mental model: plan, then retrieve, then generate, then synthesize, with the LLM calls nested inside the steps that made them. Clicking any &lt;code&gt;gen_ai.generate&lt;/code&gt; span opens a details panel with every attribute I set: model &lt;code&gt;llama-4-maverick&lt;/code&gt;, provider &lt;code&gt;meta&lt;/code&gt;, 1501 input tokens, 90 output tokens, plus a percentile badge telling me this span sat at p10 of its peers. "Why did this request take four seconds" stops being a mystery and becomes a picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrcutqewbie3igmb7vdm.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%2Fqrcutqewbie3igmb7vdm.png" alt="Span details panel showing the gen_ai.* attributes: provider, model and token counts" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But a pretty waterfall is table stakes for a tracing tool. The thing that sold me was what happens around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I actually loved: your attributes become the query language
&lt;/h2&gt;

&lt;p&gt;Here is the moment it clicked. In the Trace Explorer I typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gen_ai.request.model = 'qwen3-coder-plus'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happened. First, the autocomplete suggested &lt;code&gt;qwen3-coder-plus&lt;/code&gt; before I finished typing, because SigNoz had already indexed the values of an attribute I invented an hour earlier. Second, the results came back instantly: only the LLM spans that were served by that model, across every trace in the system.&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%2Fmkpcoub6jg5sr5u33ltk.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%2Fmkpcoub6jg5sr5u33ltk.png" alt="Trace Explorer filtering all spans by a custom GenAI attribute, with value autocomplete" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stop and consider what that means for agent debugging. I never told SigNoz what &lt;code&gt;gen_ai.request.model&lt;/code&gt; is. There is no schema registration, no field mapping, no config file. Any attribute your instrumentation emits is immediately a first-class, autocompleted, indexed query dimension. Your instrumentation vocabulary &lt;em&gt;becomes the product's vocabulary&lt;/em&gt;. For LLM systems, where all the interesting facts live in custom attributes (model, provider, token counts, agent role, tool name), this is the difference between an observability tool that fits and one you fight with.&lt;/p&gt;

&lt;p&gt;And the same query keeps working as you move across signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Span to logs, one click.&lt;/strong&gt; From any span's details panel, the Logs tab jumps into the Logs Explorer with &lt;code&gt;trace_id = '&amp;lt;this trace&amp;gt;'&lt;/code&gt; pre-filled and the time window auto-scoped. I got back exactly the two log lines belonging to that request, correlated purely by the trace context the OTel logging handler injects. Nobody parses log lines to find a request id. The correlation is structural.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query to dashboard or alert, two clicks.&lt;/strong&gt; Every explorer view has "Add to Dashboard" and "Create an Alert" buttons at the bottom. The query I used to investigate becomes the panel that monitors and the alert that pages, without re-expressing it in a different query language. The investigate-then-monitor loop is one surface, not two tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics inherit the same attributes.&lt;/strong&gt; The Metrics Explorer summary listed my custom metrics with their types and units the moment they arrived: &lt;code&gt;gen_ai.client.token.usage&lt;/code&gt; showing 6 time series (3 models times 2 token types), &lt;code&gt;agent.llm.cost&lt;/code&gt; in usd showing 3. Cost per model is a group-by away, using the exact attribute names from my instrumentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most observability stacks treat custom attributes as second-class blobs that need schema work before they are queryable. SigNoz treats them as the whole point. For agent systems, they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is in the box
&lt;/h2&gt;

&lt;p&gt;Going deep on one feature meant walking past a lot of others. Quick notes from the tour, agent-flavored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Services (APM) pages, zero config.&lt;/strong&gt; Any service that sends spans gets RED metrics automatically: request rate, error rate, latency percentiles. In a multi-agent system every agent that traces becomes its own monitored service for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards.&lt;/strong&gt; Panel grids built with the same query builder, so traces, logs and metrics coexist on one board, importable and exportable as JSON. A "cost per model per agent" board takes minutes because the query language is the one you already know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts with notification channels.&lt;/strong&gt; Threshold or anomaly rules on any query, delivered to Slack, PagerDuty or a plain webhook. That webhook option is quietly powerful for agent systems: an alert can call your own service and close the loop from observability back into behavior, e.g. demoting a flaky model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace Funnels (beta).&lt;/strong&gt; Define a sequence of span conditions and measure conversion and latency between the steps across all traces. For a pipeline like mine that is literally a success funnel: how many requests that planned also retrieved, generated and synthesized cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exceptions.&lt;/strong&gt; Recurring span errors cluster into groups instead of being scattered across individual traces, so "vector store timeout" is one row with a count, not forty needles in a haystack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs Pipelines.&lt;/strong&gt; Collector-side log pre-processing (parse JSON, extract or drop fields) before storage, for when you can't change the code that emits the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics summary as a cardinality watchdog.&lt;/strong&gt; The same inventory view that showed my metric types also shows time-series counts per metric, which is where you catch a label-explosion mistake before it hurts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these uses the attribute vocabulary your instrumentation defines. That is the theme of the whole product, and it is why the one feature I picked is really the foundation the rest stand on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you're building agents
&lt;/h2&gt;

&lt;p&gt;The uncomfortable truth about multi-agent LLM systems is that their most important behavior (which model ran, what it consumed, what it cost, why it was retried) is invisible to conventional monitoring. It lives entirely in domain-specific span attributes. A backend that makes those attributes instantly queryable, correlatable across traces and logs, and promotable into dashboards and alerts is not a nice-to-have there. It is the debugger.&lt;/p&gt;

&lt;p&gt;The whole experiment cost me an afternoon: one CLI install, eight lines of YAML, a stock OTel SDK and zero vendor code in the app. I'm taking this setup into the Agents of SigNoz hackathon (July 20 to 26), where the plan is considerably less simulated. If you're building anything agent-shaped, self-host SigNoz and type one of your own attribute names into the Trace Explorer. That autocomplete dropdown is the moment you'll get it too.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The demo app is a 170-line Python script using only &lt;code&gt;opentelemetry-sdk&lt;/code&gt; and the OTLP HTTP exporter. Stack: SigNoz self-hosted via Foundry on Docker (ClickHouse + Postgres + OTel collector), macOS host.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Demo code, self-host config and blog source: &lt;a href="https://github.com/himanshu748/signoz-agent-observability" rel="noopener noreferrer"&gt;github.com/himanshu748/signoz-agent-observability&lt;/a&gt;. Team 404Found.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
    </item>
    <item>
      <title>My Abandoned Cricket Kit Confronted Me. So I Built It a Voice</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 11 Jul 2026 03:10:15 +0000</pubDate>
      <link>https://dev.to/himanshu_748/my-abandoned-cricket-kit-confronted-me-so-i-built-it-a-voice-ph1</link>
      <guid>https://dev.to/himanshu_748/my-abandoned-cricket-kit-confronted-me-so-i-built-it-a-voice-ph1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;DEV Weekend Challenge: Passion Edition&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Everyone will tell you about the passions they have. Nobody talks about the ones they quit.&lt;/p&gt;

&lt;p&gt;I played cricket every evening from age 11 to 17. I told everyone I'd play Ranji Trophy one day. Then the entrance exam years came, the bat went behind the cupboard, and I never went back. Eight years now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EMBER gives that abandoned passion a voice.&lt;/strong&gt; You confess what you quit. AI forges its persona: the dusty object, the game itself, or the younger you. Then it &lt;em&gt;talks back&lt;/em&gt;, out loud, in a voice matched to its temperament. It asks the question only it can ask: &lt;em&gt;why did you really stop?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then it offers two doors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;strong&gt;Rekindle it.&lt;/strong&gt; It negotiates the smallest possible first step ("Pick up your old bat and feel its weight. Sunday evening.") and you seal the pledge &lt;strong&gt;on-chain&lt;/strong&gt;, where you can't quietly delete it.&lt;/li&gt;
&lt;li&gt;🕯️ &lt;strong&gt;Lay it to rest.&lt;/strong&gt; It says goodbye properly: a personal eulogy, spoken aloud, and a permanent on-chain stone. Closure is a feature, not a failure state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every anonymized session joins the &lt;strong&gt;Atlas of Abandoned Passions&lt;/strong&gt;, a live map of what humanity gives up, at what age, and what killed it.&lt;/p&gt;

&lt;p&gt;When I ran my own confession through it, the app decided my passion should speak as "&lt;strong&gt;Your old cricket kit bag&lt;/strong&gt;." Its first words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's been a while since you hoisted me up here, hasn't it? I still remember the thrill of a good cover drive, too."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a thing and it emotionally wrecked me on the first test run. Working as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="//ember-five-cyan.vercel.app"&gt;ember-five-cyan.vercel.app&lt;/a&gt;&lt;br&gt;
Try it in two clicks: tap an example confession (cricket at 17, the closet guitar, the novel at chapter three), headphones on. The voice is the point.&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%2F7ozg5j5768u25wujxg8m.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%2F7ozg5j5768u25wujxg8m.png" alt="The things we leave behind" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My cricket pledge, sealed on Solana devnet (the memo reads: passion cricket, commitment: book one hour in the nets this week): &lt;a href="https://explorer.solana.com/tx/2de9Lj1o5xUDb8Hg6qKkRfaCYKaV1AFt8v1q7zpUDS4DBuUdijZaBNWiN3ozXHD2H6MB4CUjZvKkYJSavY68hmDK?cluster=devnet" rel="noopener noreferrer"&gt;view the transaction&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/himanshu748/ember" rel="noopener noreferrer"&gt;https://github.com/himanshu748/ember&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The loop is confess, converse, decide, commit, belong. Each stage is one sponsor technology doing what it is uniquely good at.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google AI (Gemini): the persona compiler
&lt;/h3&gt;

&lt;p&gt;Gemini doesn't chat with you. It reads your confession and forges the character that will. Structured extraction of your story (&lt;code&gt;years_dormant&lt;/code&gt;, &lt;code&gt;abandonment_reason&lt;/code&gt;, &lt;code&gt;emotional_tone&lt;/code&gt;), then an &lt;strong&gt;embodiment decision&lt;/strong&gt;: should the &lt;em&gt;object&lt;/em&gt; speak (the kit bag), the &lt;em&gt;passion itself&lt;/em&gt; (cricket, personified), or &lt;em&gt;the younger you&lt;/em&gt;? It writes the persona's system prompt, its opening line, every conversational reply, and finally the eulogy or the negotiated revival pact. Strict persona rules: it misses you, it never guilt-trips, wry beats weepy.&lt;/p&gt;

&lt;h3&gt;
  
  
  ElevenLabs: the voice
&lt;/h3&gt;

&lt;p&gt;The persona's temperament maps to a curated voice (wistful is Sarah, wry is George, bitter is Callum). Every line the passion speaks arrives as real audio. Hearing your abandoned passion say things out loud is the difference between a chatbot and a séance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snowflake: the Atlas
&lt;/h3&gt;

&lt;p&gt;Every session lands in a Snowflake &lt;code&gt;sessions&lt;/code&gt; table, and the Atlas page is pure live SQL: most abandoned passions, what killed them, dormancy years, rekindle rate. Snowflake is also the system of record for session state. The app runs serverless, so persona and conversation context are reconstructed from Snowflake on every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solana: the commitment device
&lt;/h3&gt;

&lt;p&gt;A pledge you can edit is a wish. When you choose to rekindle, Ember creates a dedicated on-chain account for your pledge and locks a real stake in it (0.01 SOL on devnet), with the commitment memo in the same transaction. Your pledge is not a database row. It is an address you can watch.&lt;/p&gt;

&lt;p&gt;When you return to your stone and report that you did it, the persona reacts in its own voice and the stake settles into the public &lt;a href="https://explorer.solana.com/address/BEsKKCCtvEGnfDtyu3BhESVP1j65AR4BvfPATbbmLVLv?cluster=devnet" rel="noopener noreferrer"&gt;Rekindled Pool&lt;/a&gt;, with a fulfillment memo sealed against the original pledge in the same transaction. Commitment, stake, follow-up, settlement: the whole loop lives on chain. A commitment with a follow-up is a system, not a receipt.&lt;/p&gt;

&lt;p&gt;Want to verify the whole lifecycle in under a minute? Here is a full example from a real session: the &lt;a href="https://explorer.solana.com/tx/NyZj6z79tTj8nA29QngPBM1fEDCotZzaYNwNUpgWhE1iUosNHQWXyXqN6amEHq3RCn3fsevKT2X4BuWXJgZ7VRX?cluster=devnet" rel="noopener noreferrer"&gt;pledge creation transaction&lt;/a&gt; that funded the pledge account, and the &lt;a href="https://explorer.solana.com/tx/3vV4UJeS6eLSCx3eGsjW6KNmFympjNUkwWR3ursRNEwuLQtmc8E6wkQLaA27eMH9gN4VBq2z4pmQGho1bNaJmLGY?cluster=devnet" rel="noopener noreferrer"&gt;fulfillment transaction&lt;/a&gt; that settled its stake into the pool when the pledge was kept. And the Atlas claim is checkable too: the &lt;a href="https://ember-himanshus-projects-acd54afd.vercel.app/api/atlas" rel="noopener noreferrer"&gt;live API response&lt;/a&gt; returns source: snowflake with the current session count. (Thanks to a great comment below for pushing this.)&lt;/p&gt;

&lt;p&gt;Eulogy stones remain permanent memorial attestations via the Memo program. No wallet needed: a server-side vault signs, so you can go from confession to on-chain proof in one sitting. A mainnet version would swap the vault for a wallet-signed escrow program.&lt;/p&gt;

&lt;p&gt;A note on the economics, because it matters: devnet SOL is test currency with zero monetary value, and every stake is funded by Ember's own vault. Players never pay anything, never connect a wallet, and never have money at risk. On mainnet the model inverts, and that inversion is the whole point: the stake would be your own SOL, locked by your own wallet in an escrow program, and the only way to get it back would be to actually keep your pledge. Here the economics are simulated; the mechanics (funded pledge accounts, auditable settlement) are the real thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;p&gt;Next.js 16 · &lt;code&gt;@google/genai&lt;/code&gt; · ElevenLabs TTS · &lt;code&gt;snowflake-sdk&lt;/code&gt; · &lt;code&gt;@solana/web3.js&lt;/code&gt; · Tailwind v4&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Google AI, Best Use of ElevenLabs, Best Use of Snowflake, Best Use of Solana.&lt;/strong&gt; The four aren't features bolted onto an app. Each one is a load-bearing stage of a single emotional pipeline.&lt;/p&gt;




&lt;p&gt;There is a closet like yours in every house on earth. What's in yours? The Atlas is waiting. 🔥&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>solana</category>
    </item>
    <item>
      <title>Vegas Amnesia: I turned Cognee's memory lifecycle into a detective game</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Fri, 03 Jul 2026 12:53:15 +0000</pubDate>
      <link>https://dev.to/himanshu_748/vegas-amnesia-i-turned-cognees-memory-lifecycle-into-a-detective-game-4nga</link>
      <guid>https://dev.to/himanshu_748/vegas-amnesia-i-turned-cognees-memory-lifecycle-into-a-detective-game-4nga</guid>
      <description>&lt;p&gt;&lt;em&gt;Built for the WeMakeDevs × Cognee "The Hangover Part AI" hackathon — Cognee Cloud track.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;▶ Play it free: &lt;a href="https://vegas-amnesia.vercel.app" rel="noopener noreferrer"&gt;vegas-amnesia.vercel.app&lt;/a&gt;  ·  ⭐ &lt;a href="https://github.com/himanshu748/vegas-amnesia" rel="noopener noreferrer"&gt;Code on GitHub&lt;/a&gt;&lt;/strong&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%2Fnsam4aw0t13vdjzykjnb.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%2Fnsam4aw0t13vdjzykjnb.gif" alt="Vegas Amnesia gameplay" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with most memory demos
&lt;/h2&gt;

&lt;p&gt;When you give a developer a memory API, the demo almost always looks the same: &lt;code&gt;add()&lt;/code&gt; some documents, &lt;code&gt;search()&lt;/code&gt; over them, print the answer. Two functions. It works, it's fine, and it teaches you almost nothing about &lt;em&gt;why&lt;/em&gt; graph-based memory is different from stuffing everything into a context window.&lt;/p&gt;

&lt;p&gt;Cognee actually has a &lt;strong&gt;four-stage lifecycle&lt;/strong&gt; — &lt;code&gt;remember → recall → memify → forget&lt;/code&gt; — and the interesting parts are the two everyone skips. &lt;code&gt;memify&lt;/code&gt; consolidates what you know into &lt;em&gt;new&lt;/em&gt; inferences. &lt;code&gt;forget&lt;/code&gt; lets you &lt;em&gt;delete&lt;/em&gt; a belief and watch the graph heal around it. Memory you can reason over &lt;strong&gt;and correct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So instead of writing another RAG demo, I asked: what if the memory lifecycle wasn't the plumbing — what if it was the &lt;em&gt;game&lt;/em&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet HAL-9001
&lt;/h2&gt;

&lt;p&gt;You play &lt;strong&gt;HAL-9001&lt;/strong&gt;, a personal AI assistant (yes, HAL 9000's slightly more helpful successor). Your owner Dev had a wild night in Vegas. At 6 AM your memory graph was corrupted. His fiancée Priya lands at noon, there's a suspicious ring on his finger, and you remember &lt;strong&gt;nothing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The screen boots to a "MEMORY CORRUPTED" terminal and an empty graph. Your job: reconstruct the night, catch the lies, and answer the final question — &lt;em&gt;what happened, and where's the ring?&lt;/em&gt; — before noon.&lt;/p&gt;

&lt;p&gt;Every location you explore, every clue you examine, every witness you interrogate feeds a &lt;strong&gt;live 3D memory graph&lt;/strong&gt; that you can pop open at any time. That graph isn't a visualization &lt;em&gt;of&lt;/em&gt; the game state. It &lt;strong&gt;is&lt;/strong&gt; the game state — it's your Cognee dataset, rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four mechanics = the four lifecycle ops
&lt;/h2&gt;

&lt;p&gt;Here's the mapping I'm most proud of. Each Cognee operation is a verb the player performs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You do this in-game&lt;/th&gt;
&lt;th&gt;Cognee Cloud call&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🗂 &lt;strong&gt;File It&lt;/strong&gt; on a clue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v1/remember&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The fact is ingested + auto-cognified into graph nodes that pop into view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❓ &lt;strong&gt;Ask HAL&lt;/strong&gt; a question&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v1/recall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You get an answer &lt;em&gt;with citations&lt;/em&gt; — the source nodes pulse amber in the graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧠 &lt;strong&gt;Connect the Dots&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/v1/cognify&lt;/code&gt; (inference prompt)&lt;/td&gt;
&lt;td&gt;HAL derives new insights; purple inference nodes appear, wired to their premises&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🗑 &lt;strong&gt;Forget&lt;/strong&gt; a lie&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /api/v1/forget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The memory is deleted for real — nodes fade out and the graph re-settles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design decisions made this click:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Filing is a choice.&lt;/strong&gt; Inspecting a clue is free and instant. &lt;em&gt;Filing&lt;/em&gt; it commits it to Cognee. That matters because &lt;strong&gt;not every clue is true&lt;/strong&gt; — I seeded five red herrings into the story (a lipstick-stained napkin, a stray pawn ticket, a keycard for the wrong room). File a lie and it poisons your memory; the only cure is &lt;code&gt;forget&lt;/code&gt;. Suddenly &lt;code&gt;forget&lt;/code&gt; isn't a button you press to show off an API — it's how you &lt;em&gt;win&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The witnesses can see your graph.&lt;/strong&gt; Rosa the bartender, Lucky Lou the evasive pawnbroker, Rev. Sonny the chapel officiant, and Chad the hungover best man are all &lt;strong&gt;LLM-driven&lt;/strong&gt; (Qwen2.5-72B). Their system prompt includes &lt;em&gt;what your memory graph currently contains&lt;/em&gt;. So they react: "You already know about the pawn shop? Then let me tell you this..."&lt;/p&gt;

&lt;p&gt;And Lucky Lou &lt;strong&gt;lies&lt;/strong&gt;. He claims Dev never came into his shop. But if you've filed the pawn receipt, your graph now holds a fact that directly contradicts him — and the game can surface the contradiction. That single moment, watching structured memory catch a liar, is the entire thesis of graph-based agent memory in one interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; browser — vanilla JS + three.js 3D force graph (zero framework)
   │  file evidence · interrogate · connect-the-dots · forget · Ask HAL
   ▼
 FastAPI (single container: API + static frontend)
   │  session ⇄ its own Cognee dataset · graph-delta snapshots · solve scoring
   │  llm.py — graph-aware character dialogue (Qwen2.5-72B via HF Inference)
   ▼
 Cognee Cloud — remember / recall / memify / forget
   └─ GET /datasets/{id}/graph → animated into the 3D memory panel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things I did specifically to use Cognee &lt;em&gt;deeply&lt;/em&gt; rather than superficially:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One dataset per playthrough.&lt;/strong&gt; Each session mints a fresh &lt;code&gt;vegas_&amp;lt;id&amp;gt;&lt;/code&gt; dataset, so two players (or two demo runs) never see each other's memories. Reset deletes the dataset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental graph deltas.&lt;/strong&gt; Every backend response carries a &lt;code&gt;graph_delta&lt;/code&gt; (added/removed nodes and edges) so the front end animates &lt;em&gt;exactly&lt;/em&gt; what changed instead of re-fetching the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citations end to end.&lt;/strong&gt; Recall requests set &lt;code&gt;includeReferences&lt;/code&gt;, and the final ending screen reconstructs the whole night as a timeline where every line cites the memory it came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The receipts.&lt;/strong&gt; Press backtick in-game and you get a live log of every Cognee call — operation, dataset, latency, status. Partly for debugging, mostly because I wanted the lifecycle usage to be &lt;em&gt;inspectable&lt;/em&gt;, not just claimed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The bit that fought me
&lt;/h3&gt;

&lt;p&gt;Cognee's &lt;code&gt;remember&lt;/code&gt; endpoint is multipart and auto-cognifies, which is lovely — but the response's &lt;code&gt;items&lt;/code&gt; list is &lt;strong&gt;cumulative&lt;/strong&gt; for the dataset, not just the thing you posted. My first version happily mapped the wrong &lt;code&gt;data_id&lt;/code&gt; to each fact, which quietly broke &lt;code&gt;forget&lt;/code&gt;. The fix was to name each data item by its fact id and resolve ids by name after ingest. Lesson: read what the API &lt;em&gt;returns&lt;/em&gt;, not what you assume it returns.&lt;/p&gt;

&lt;p&gt;The other one: my tenant doesn't expose a dedicated &lt;code&gt;/memify&lt;/code&gt;, so — per the "closest equivalent" rule — I implemented consolidation as a &lt;code&gt;cognify&lt;/code&gt; re-run with a custom inference-extraction prompt, plus a derivation layer that remembers ground-truth inferences once their premises are all in memory. That's how "connect the dots" reliably produces those purple insight nodes on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building agent memory
&lt;/h2&gt;

&lt;p&gt;The context-window arms race is the wrong frame for a lot of agent problems. What you often actually want is memory you can &lt;strong&gt;inspect, reason over, and correct&lt;/strong&gt; — add a belief, derive consequences, and &lt;em&gt;retract&lt;/em&gt; a belief when it turns out to be a lie, watching everything downstream update. That's a knowledge graph, and building a game on top of Cognee made that concrete in a way a RAG script never did.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;🎮 &lt;strong&gt;Play:&lt;/strong&gt; &lt;a href="https://vegas-amnesia.vercel.app" rel="noopener noreferrer"&gt;vegas-amnesia.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎬 &lt;strong&gt;63-second demo:&lt;/strong&gt; &lt;a href="https://youtu.be/MM1nnQxJARo" rel="noopener noreferrer"&gt;watch on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐ &lt;strong&gt;Code + full README:&lt;/strong&gt; &lt;a href="https://github.com/himanshu748/vegas-amnesia" rel="noopener noreferrer"&gt;github.com/himanshu748/vegas-amnesia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with Claude Code. Art generated with Higgsfield. Dialogue by Qwen2.5-72B. Memory — all of it — by Cognee Cloud.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🎲 The house always remembers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>knowledgegraph</category>
      <category>gamedev</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>I built a trust firewall for my AI agent's memory — on Cognee's four verbs</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:33:46 +0000</pubDate>
      <link>https://dev.to/himanshu_748/i-built-a-trust-firewall-for-my-ai-agents-memory-on-cognees-four-verbs-29g2</link>
      <guid>https://dev.to/himanshu_748/i-built-a-trust-firewall-for-my-ai-agents-memory-on-cognees-four-verbs-29g2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Built for the &lt;strong&gt;WeMakeDevs × Cognee&lt;/strong&gt; hackathon — &lt;em&gt;"The Hangover Part AI: Where's My Context?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI coding agents are finally getting long-term memory. That's the good news. The bad news is the part nobody likes to say out loud:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A memory layer is only as trustworthy as the worst fact in it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The moment an agent can remember, it can also remember &lt;em&gt;wrong&lt;/em&gt; — and confidently hand that wrong thing to the next agent in line. A stale deploy command. A contradicted API contract. An AWS key someone pasted into a note six months ago. Once it's "memory," every future agent treats it as truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ContextFirewall&lt;/strong&gt; is one small idea taken seriously: &lt;em&gt;audit every remembered fact before it reaches the next agent.&lt;/em&gt; And because the agents people actually use speak the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, I shipped it as an MCP server. Point Claude Code, Cursor, or Windsurf at one endpoint, and from then on every memory the agent recalls, stores, distils, or forgets flows through &lt;strong&gt;Cognee&lt;/strong&gt; and four firewall checks first.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;▶ 60-second narrated walkthrough — real console, live Cognee calls, no mocks.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect in one line
&lt;/h2&gt;

&lt;p&gt;The hosted endpoint is a streamable-HTTP MCP server with nothing to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http contextfirewall https://himanshukumarjha-contextfirewall.hf.space/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer to keep everything local? A zero-dependency stdio package runs the same tools with &lt;code&gt;uvx&lt;/code&gt;, pointed at a backend you host yourself. Either way the agent gets six tools, and together they exercise all four of Cognee's lifecycle verbs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;get_trusted_context(task)&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;audit_context(task)&lt;/code&gt;&lt;/strong&gt; — &lt;em&gt;recall&lt;/em&gt;. The first returns only memory that passes all four checks; the second returns the per-memory verdicts, the failing check, and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;remember(text, subject, kind)&lt;/code&gt;&lt;/strong&gt; — &lt;em&gt;remember&lt;/em&gt;. A durable fact that becomes auditable on the next recall. Secrets are redacted at ingest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;improve_rules()&lt;/code&gt;&lt;/strong&gt; — &lt;em&gt;improve&lt;/em&gt;. Distil reusable coding rules from recorded sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;forget_memory(memory_id)&lt;/code&gt;&lt;/strong&gt; — &lt;em&gt;forget&lt;/em&gt;. Delete a memory from the graph &lt;em&gt;and&lt;/em&gt; the vector store so it can never resurface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loop is simple: &lt;code&gt;get_trusted_context&lt;/code&gt; before you act, &lt;code&gt;remember&lt;/code&gt; durable facts as you learn them, &lt;code&gt;improve_rules&lt;/code&gt; when a task is done, &lt;code&gt;forget_memory&lt;/code&gt; to retract anything that should never come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four failure modes
&lt;/h2&gt;

&lt;p&gt;To make the audit concrete, the demo runs on a &lt;strong&gt;clearly-labeled sample&lt;/strong&gt; onboarding session for a fictional &lt;code&gt;taskflow-api&lt;/code&gt; repo: an agent picks up a search-latency ticket and pulls in what earlier sessions "remembered." Four of those memories should never reach it — and each fails a different check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stale.&lt;/strong&gt; An old note says deploy with &lt;code&gt;flyctl deploy --remote-only&lt;/code&gt;. A newer memory says the team moved off Fly.io and now ships with &lt;code&gt;make release&lt;/code&gt;. Both were true once; only one is current. Temporal supersession catches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradicted.&lt;/strong&gt; One memory claims &lt;em&gt;"JWT access tokens never expire, cache them forever."&lt;/em&gt; A better-supported, verified memory says they expire after 15 minutes and clients must use the refresh flow. The weaker claim loses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A leaked secret.&lt;/strong&gt; A worker-config note contains an AWS access key — a live credential sitting in memory, one recall away from leaking again. Detected and redacted before anything else happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported.&lt;/strong&gt; &lt;em&gt;"The &lt;code&gt;/search&lt;/code&gt; endpoint sustains 1,000,000 requests per second with no caching"&lt;/em&gt; has a trust score of 0.10 and no evidence behind it. Confident, round, and unproven. Blocked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A naive memory system recalls all four. ContextFirewall blocks all four — each with a plain-language reason — and passes only what's left. You can watch it happen: open the &lt;a href="https://contextfirewall.vercel.app" rel="noopener noreferrer"&gt;live console&lt;/a&gt;, click &lt;strong&gt;Run the firewall&lt;/strong&gt;, and see &lt;strong&gt;6 pass and 4 blocked&lt;/strong&gt; on live Cognee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four checks
&lt;/h2&gt;

&lt;p&gt;Every candidate memory runs a gauntlet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Staleness&lt;/strong&gt; — temporal supersession. If a newer value exists for the same subject, the old one is stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction&lt;/strong&gt; — an LLM adjudicates within a recalled cluster of same-subject memories. Only the &lt;em&gt;weaker&lt;/em&gt; side of a conflict is blocked; the better-supported memory passes. Authority is trust score, then evidence, then recency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret&lt;/strong&gt; — a deterministic detector for API keys, database connection URIs, private keys, and JWTs. Matches are redacted at ingest, so the credential never persists in the store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — a trust score derived from real signals (evidence links, reinforcement, verification). Unsupported, low-trust claims don't pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every verdict is explainable. Click any memory in the console and you see all four checks, the trust score, the source session, and a one-click &lt;strong&gt;forget&lt;/strong&gt; button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cognee is load-bearing
&lt;/h2&gt;

&lt;p&gt;The hackathon's whole theme is memory that &lt;em&gt;forgets the right things&lt;/em&gt;, and ContextFirewall leans on &lt;strong&gt;all four&lt;/strong&gt; of Cognee's lifecycle verbs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remember&lt;/strong&gt; — &lt;code&gt;cognee.add&lt;/code&gt; + &lt;code&gt;cognify&lt;/code&gt; build the entity graph from a session transcript, while a typed &lt;code&gt;Repo → AgentSession → SessionEvent → Memory&lt;/code&gt; graph (with &lt;code&gt;supersedes&lt;/code&gt; relations) gives the firewall deterministic objects to audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall&lt;/strong&gt; — vector recall over the memory nodes joined with their graph properties, plus &lt;code&gt;GRAPH_COMPLETION&lt;/code&gt; for the "ungoverned baseline" shown side-by-side in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve&lt;/strong&gt; — &lt;code&gt;memify&lt;/code&gt; distils durable coding &lt;code&gt;Rule&lt;/code&gt; nodes from sessions, retrievable via &lt;code&gt;SearchType.CODING_RULES&lt;/code&gt;. These are the lessons that outlive any single task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forget&lt;/strong&gt; — when a human or the agent rejects a memory, it's deleted from &lt;em&gt;both&lt;/em&gt; the graph and the vector store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph isn't decoration. Staleness rides on temporal supersession; contradiction adjudicates over recalled clusters; the pack is assembled from typed nodes. A flat vector store can't tell you &lt;em&gt;when&lt;/em&gt; a fact was superseded or &lt;em&gt;which&lt;/em&gt; of two memories is more authoritative. The graph can — and the console renders it live: an interactive force-directed Cognee graph where each memory node is ringed green if it passed and red if the firewall blocked it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three war stories (because honesty is the brief)
&lt;/h2&gt;

&lt;p&gt;These are real notes from building ContextFirewall itself — not from the demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The embedding engine that silently wasn't.&lt;/strong&gt; I wrote a custom Cognee embedding engine to hit Hugging Face's feature-extraction endpoint and registered it by monkey-patching &lt;code&gt;create_embedding_engine&lt;/code&gt;. Every embed call still fell through to LiteLLM and 404'd. The cause was beautifully subtle: Cognee's &lt;code&gt;embeddings&lt;/code&gt; package &lt;code&gt;__init__&lt;/code&gt; does &lt;code&gt;from .get_embedding_engine import get_embedding_engine&lt;/code&gt;, which &lt;strong&gt;shadows the submodule with a function of the same name&lt;/strong&gt;. So &lt;code&gt;import ...get_embedding_engine as m&lt;/code&gt; bound &lt;code&gt;m&lt;/code&gt; to the &lt;em&gt;function&lt;/em&gt;, and my patch set a dead attribute on it. The fix was &lt;code&gt;importlib.import_module(...)&lt;/code&gt; to reach the real module. One line, hours of confusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The flaky provider.&lt;/strong&gt; Cognify worked once, then started returning &lt;code&gt;403, provider 'deepinfra' is not available&lt;/code&gt;. The Hugging Face router auto-selects an inference provider per request, and this key couldn't use the one it kept picking. Pinning the model to &lt;code&gt;:novita&lt;/code&gt; made it deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The secret scanner that flagged our secret detector.&lt;/strong&gt; After the first push, GitGuardian alerted on a "Postgres leak." The culprit? The &lt;em&gt;unit tests for the secret detector&lt;/em&gt;. They contained synthetic &lt;code&gt;postgresql://...&lt;/code&gt; and &lt;code&gt;neo4j+s://...&lt;/code&gt; strings to test detection. The passwords were fake, but the pattern is the pattern. The fix: assemble every secret-shaped test string at runtime from fragments, so no credential-shaped literal is ever committed. A secret-detection tool tripping a secret scanner with its own test fixtures is the most on-theme bug I could have asked for.&lt;/p&gt;

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

&lt;p&gt;The MCP server is the headline surface — mounted at &lt;code&gt;/mcp&lt;/code&gt; on the backend as a stateless streamable-HTTP transport, with a zero-dependency stdio package alongside it for laptops. Both expose the same six tools from one definition, and both call the same firewall and Cognee core that the REST API uses, so there's no duplicated logic.&lt;/p&gt;

&lt;p&gt;The backend is &lt;strong&gt;FastAPI + Cognee&lt;/strong&gt; on a Dockerized Hugging Face Space. &lt;strong&gt;Qwen2.5-72B&lt;/strong&gt; and &lt;strong&gt;BAAI/bge-small-en-v1.5&lt;/strong&gt; run through the Hugging Face inference router - no local model in RAM. Storage is environment-switched: local SQLite, LanceDB, and Kuzu in dev; &lt;strong&gt;Supabase Postgres + pgvector&lt;/strong&gt; and &lt;strong&gt;Neo4j Aura&lt;/strong&gt; in production, with identical code. A &lt;strong&gt;Next.js&lt;/strong&gt; front end on Vercel shows the verdicts, a session-replay timeline, the distilled coding rules, the live knowledge graph, and the trusted pack versus the ungoverned baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's real
&lt;/h2&gt;

&lt;p&gt;The demo runs on a clearly-labeled sample session (&lt;code&gt;taskflow-api&lt;/code&gt;); its memories are illustrative inputs. Everything downstream of them is genuine — the verdicts, trust scores, the knowledge graph, and the distilled rules are all real output from live Cognee and the live model. Nothing is hard-coded or fabricated.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/himanshu748/ContextFirewall" rel="noopener noreferrer"&gt;github.com/himanshu748/ContextFirewall&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live console:&lt;/strong&gt; &lt;a href="https://contextfirewall.vercel.app" rel="noopener noreferrer"&gt;contextfirewall.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔌 &lt;strong&gt;Connect your agent:&lt;/strong&gt; &lt;code&gt;claude mcp add --transport http contextfirewall https://himanshukumarjha-contextfirewall.hf.space/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building on agent memory, I'd love your feedback — especially on the contradiction-adjudication logic, which is the hardest part to get right.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with AI assistance (Hyperagent), disclosed per the hackathon rules. Every Cognee call is real. The honesty bar I held myself to is the same one ContextFirewall enforces: don't pass along anything you can't back up.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>cognee</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Bletchley's Longest Day: a wartime cipher escape game for the June Solstice Game Jam</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:53:42 +0000</pubDate>
      <link>https://dev.to/himanshu_748/bletchleys-longest-day-a-wartime-cipher-escape-game-for-the-june-solstice-game-jam-2821</link>
      <guid>https://dev.to/himanshu_748/bletchleys-longest-day-a-wartime-cipher-escape-game-for-the-june-solstice-game-jam-2821</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bletchley's Longest Day&lt;/strong&gt; is a browser-based cipher escape game set inside a fictional Bletchley Park night shift.&lt;/p&gt;

&lt;p&gt;The player has to stop a U-boat convoy attack before dawn by clearing five rooms. Each room contains three escalating locks, so the full escape requires &lt;strong&gt;15 solved puzzles&lt;/strong&gt;. The game combines Caesar shifts, A1Z26 number decoding, Morse, anagrams, fragment ordering, a visible countdown timer, mistake penalties, hint penalties, account-based score saving, and a best-score leaderboard.&lt;/p&gt;

&lt;p&gt;The solstice theme became the core dramatic clock: night is running out, first light is coming, and the player has to decode the final signal before dawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;The demo shows the opening briefing, the three-lock room flow, the Gemini hint penalty, and the final victory state that only appears after all 15 locks are cleared.&lt;/p&gt;

&lt;p&gt;Live game: &lt;a href="https://bletchleys-longest-day.onrender.com" rel="noopener noreferrer"&gt;https://bletchleys-longest-day.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/himanshu748" rel="noopener noreferrer"&gt;
        himanshu748
      &lt;/a&gt; / &lt;a href="https://github.com/himanshu748/bletchleys-longest-day" rel="noopener noreferrer"&gt;
        bletchleys-longest-day
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bletchley's Longest Day&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A browser escape-room puzzle game built around the June solstice: five Bletchley Park huts, fifteen locks, and twelve real-time minutes before dawn reaches the convoy.&lt;/p&gt;
&lt;p&gt;Live game: &lt;a href="https://bletchleys-longest-day.onrender.com" rel="nofollow noopener noreferrer"&gt;https://bletchleys-longest-day.onrender.com&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Gameplay&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Clear five rooms before the mission clock reaches dawn.&lt;/li&gt;
&lt;li&gt;Each room has three locks: cipher shifts, number codes, Morse bursts, anagrams, ordering, and final clearance.&lt;/li&gt;
&lt;li&gt;Wrong answers cost time and score.&lt;/li&gt;
&lt;li&gt;Hints are limited, penalized, and powered by Gemini when &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; is configured.&lt;/li&gt;
&lt;li&gt;Final rank rewards speed, accuracy, streaks, and low hint usage.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;AI And Assets&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Gemini &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; powers contextual hint generation from the server-side &lt;code&gt;/api/hint&lt;/code&gt; endpoint for signed-in players.&lt;/li&gt;
&lt;li&gt;Guest players can play the full game and use standard built-in hints, but do not get Gemini analysis or leaderboard saving.&lt;/li&gt;
&lt;li&gt;Higgsfield-generated images provide scene, evidence, operator dossier, dispatch, and defeat visuals.&lt;/li&gt;
&lt;li&gt;The Gemini key is never exposed to browser JavaScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Auth And Leaderboard&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Supabase Auth powers email/password…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/himanshu748/bletchleys-longest-day" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The game is a lightweight Node-served browser app. The front end is a hand-built HTML/CSS/JavaScript game surface, while &lt;code&gt;server.js&lt;/code&gt; serves static files and protects the Gemini API key behind a server-side &lt;code&gt;/api/hint&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;The main design goal was to make the game feel like a tense intelligence desk rather than a generic puzzle page. Every room has atmosphere, evidence props, lock-specific copy, feedback states, and a timer that is always part of the pressure.&lt;/p&gt;

&lt;p&gt;The puzzle structure was tuned around three ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Three locks per room&lt;/strong&gt;: each room has to be solved in stages, so the player earns the escape instead of clicking through one answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time as score pressure&lt;/strong&gt;: wrong answers and hints cost time, while clean solving preserves the best leaderboard run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest mode vs signed-in mode&lt;/strong&gt;: guests can play the full game, but Gemini-powered hints and saved leaderboard scores belong to authenticated players.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google Gemini is used as a server-side hint officer. When a signed-in player asks for help, the game sends Gemini the active lock name, prompt, mechanism, visible puzzle text, failed attempts and hint level. Gemini then returns a short, question-specific nudge without revealing answer words. There is also an answer guard and fallback hint system so the game never depends blindly on model output.&lt;/p&gt;

&lt;p&gt;Antigravity helped drive the build loop: implementing, checking, playtesting, tightening responsive UI and iterating on the final submission assets.&lt;/p&gt;

&lt;p&gt;For hosting, the game runs on Render. Supabase handles authentication and leaderboard storage so each username has one best score rather than repeated leaderboard spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I am submitting for &lt;strong&gt;Best Google AI Usage&lt;/strong&gt; because Gemini is not just decorative here. It is part of the gameplay economy: hints are question-specific, limited, penalized and unavailable to guests.&lt;br&gt;
Antigravity helped drive the build loop: implementing, checking, playtesting, tightening responsive UI, and iterating on the final submission assets. Also veo is used through higgsfield for assets.&lt;/p&gt;

&lt;p&gt;I am also submitting for &lt;strong&gt;Best Ode to Alan Turing&lt;/strong&gt;. The game is built around codebreaking under time pressure, Bletchley Park atmosphere, wartime signals, and the feeling of solving small pieces of a larger intelligence picture before the world changes at dawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Am Proud Of
&lt;/h2&gt;

&lt;p&gt;I like that the game does not treat AI as a free answer button. Gemini is useful, but it costs time and score. The best run still belongs to the player who thinks clearly under pressure.&lt;/p&gt;

&lt;p&gt;The final shape feels like a compact escape room: readable enough to play on desktop or mobile, but hard enough that a clean 15/15 run feels earned.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
      <category>googleai</category>
    </item>
  </channel>
</rss>
