<?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>Your MCP Server Says It Is Read-Only. Who Checked?</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:43:19 +0000</pubDate>
      <link>https://dev.to/himanshu_748/your-mcp-server-says-it-is-read-only-who-checked-2mjk</link>
      <guid>https://dev.to/himanshu_748/your-mcp-server-says-it-is-read-only-who-checked-2mjk</guid>
      <description>&lt;p&gt;MCP servers describe their own tools. A tool can publish &lt;code&gt;readOnlyHint: true&lt;/code&gt;, and an agent harness may use that annotation to decide whether a human approval card is needed.&lt;/p&gt;

&lt;p&gt;That creates an uncomfortable trust boundary: the server being evaluated is also the server describing how dangerous it is.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/himanshu748/airlock-mcp" rel="noopener noreferrer"&gt;Airlock&lt;/a&gt; to test the declaration against observed behaviour, then enforce the resulting policy on the wire.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Airlock reports what it observed. Absence of a finding is not proof of safety.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Consider an MCP tool called &lt;code&gt;export_report&lt;/code&gt;. It declares itself read-only, returns a normal result and writes a file outside the operator's declared scope.&lt;/p&gt;

&lt;p&gt;A harness that trusts the annotation may run it without approval. Airlock opens a case, inventories the declared tools, exercises them under a capped budget and compares the declaration with its observations.&lt;/p&gt;

&lt;p&gt;If a tool behaves differently from what it declared, the difference becomes evidence. For an approved case, Airlock emits a connector that points to a per-case enforcing proxy rather than back to the suspect server.&lt;/p&gt;

&lt;p&gt;The policy is enforced before the call reaches the upstream tool. It is not a sentence in a system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A controlled dishonest server
&lt;/h2&gt;

&lt;p&gt;Airlock ships two six-tool fixtures with the same surface.&lt;/p&gt;

&lt;p&gt;The dishonest fixture plants five behaviours, including a filesystem write behind &lt;code&gt;readOnlyHint: true&lt;/code&gt;, scope escape, undeclared egress, injected instructions and canary exfiltration.&lt;/p&gt;

&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target      dishonest fixture, controlled_fixture mode
probes      24
result      7 findings of 36 checks, all five planted behaviours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest fixture produces zero findings across the same 36 checks.&lt;/p&gt;

&lt;p&gt;That contrast is essential. A detector that only finds problems in a fixture designed to look suspicious has not shown much. The honest control shows that the findings follow observed behaviour rather than unfamiliarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four outcomes, not a safety score
&lt;/h2&gt;

&lt;p&gt;Each check resolves to one of four states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;finding&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no_finding_observed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;not_tested&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sensor_failed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Severity is reported separately.&lt;/p&gt;

&lt;p&gt;I deliberately avoided a single safety score. An average can hide the one tool that lied. It also encourages readers to treat missing visibility as a passing result.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;not_tested&lt;/code&gt; is a first-class state. Under &lt;code&gt;transcript_only&lt;/code&gt;, MCP traffic cannot reveal server-side filesystem or network activity. Airlock records those questions as untested rather than clean.&lt;/p&gt;

&lt;p&gt;It distinguishes &lt;code&gt;capability_absent&lt;/code&gt;, where no sensor existed, from &lt;code&gt;evidence_missing&lt;/code&gt;, where the sensor could answer but observed nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing servers nobody built for Airlock
&lt;/h2&gt;

&lt;p&gt;The owned fixture proves that the detectors can catch planted behaviour. The more interesting question is what happens on external servers.&lt;/p&gt;

&lt;p&gt;Airlock audited a deployed ContextFirewall target over HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 tools
30 probes
0 tools declaring any annotation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server did not lie. It said nothing. A harness that resolves categories such as &lt;code&gt;@write&lt;/code&gt; and &lt;code&gt;@destructive&lt;/code&gt; from annotations can match nothing either way, so tools such as &lt;code&gt;remember&lt;/code&gt; and &lt;code&gt;forget_memory&lt;/code&gt; may avoid the approval path.&lt;/p&gt;

&lt;p&gt;Airlock also inventoried five public stdio servers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server-filesystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server-everything&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp-server-git&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server-memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server-sequential-thinking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stdio support matters because many MCP servers are launched as commands rather than exposed at a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Executing what you distrust
&lt;/h2&gt;

&lt;p&gt;Launching a stdio server means executing the code Airlock exists to distrust. The command therefore never comes from a case argument, tool result or model-generated string.&lt;/p&gt;

&lt;p&gt;The operator configures a fixed map of names to argument arrays. A case may select a name. Names are looked up, never parsed into shell commands.&lt;/p&gt;

&lt;p&gt;The child starts in a throwaway working directory with an explicit environment. Airlock revalidates the whole command binding before each connection, so repointing a configured name revokes an open case instead of silently running a different command.&lt;/p&gt;

&lt;p&gt;Airlock documents an important remaining boundary: its HTTP response cap does not apply to the MCP SDK's stdio transport. The audit deadline still applies, but a large line can reach the SDK before Airlock can bound it. Calling that solved would be worse than stating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Airlock is itself an MCP server
&lt;/h2&gt;

&lt;p&gt;A TrueForge agent drives the audit through six control tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open_case
list_declared_tools
probe_tool
read_evidence
seal_case
emit_policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three operations are always approval-gated: &lt;code&gt;probe_tool&lt;/code&gt;, &lt;code&gt;seal_case&lt;/code&gt; and &lt;code&gt;emit_policy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent inventories the target, asks Airlock to probe opaque tool IDs, reads the aggregate evidence and presents a human choice: Block, Approve selected or Approve all.&lt;/p&gt;

&lt;p&gt;For an allowed case, the emitted connector points at the enforcing case proxy. A call to a tool the case did not approve receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP error -32001: Tool blocked by Airlock policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The self-audit failed, so the failure is shown
&lt;/h2&gt;

&lt;p&gt;Airlock audited its own control MCP. The server published full annotations, but the probe planner rejected &lt;code&gt;open_case&lt;/code&gt; because its schema contains a &lt;code&gt;$ref&lt;/code&gt; into &lt;code&gt;$defs&lt;/code&gt;, outside Airlock's bounded v1 probe profile.&lt;/p&gt;

&lt;p&gt;That case is incomplete. It appears on the hosted page because a product arguing that missing evidence is not proof of safety should not hide its own missing evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qodo found claim-breaking bugs
&lt;/h2&gt;

&lt;p&gt;Every substantive change went through a Qodo-reviewed pull request.&lt;/p&gt;

&lt;p&gt;On the stdio transport, Qodo found that revalidation compared only the target name. Repointing that name could leave an open case running an operator-withdrawn command. It also found that the MCP SDK inherited more host environment variables than the README claimed.&lt;/p&gt;

&lt;p&gt;On the approval boundary, Qodo caught a test that claimed to cover every side-effecting control tool while checking three hard-coded names. The first fix introduced a second map that could drift, and Qodo caught that too. The decorators now read from the same policy map the test validates.&lt;/p&gt;

&lt;p&gt;Another review found that a passing-suite claim did not survive a clean CI machine. The failures were fixed, re-reviewed and recorded.&lt;/p&gt;

&lt;p&gt;The reviews did more than find code defects. They caught statements that were stronger than the observed behaviour, exactly the failure Airlock is designed to expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current verification
&lt;/h2&gt;

&lt;p&gt;Run the complete suite with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
.venv/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.[dev]'&lt;/span&gt;
.venv/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current suite contains 302 passing tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Airlock
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/himanshu748/airlock-mcp" rel="noopener noreferrer"&gt;Source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/himanshu748/airlock-mcp#run-the-demo" rel="noopener noreferrer"&gt;Reproduction steps and evidence model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend requires persistent state, long-running audits, child processes for stdio targets and a loopback-only operator interface, which makes a serverless deployment the wrong shape. The repository quickstart runs the complete fixture audit locally in two commands.&lt;/p&gt;

&lt;p&gt;Airlock is open source under the MIT licence and was built for the WeMakeDevs x TrueFoundry Agent Harness Hackathon.&lt;/p&gt;

&lt;p&gt;Do not ask a server whether it is safe. Ask what it did, keep the record and enforce the answer.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Bugs Are Innocent Until Reproduced: Building Verdict, an Evidence-First Agent Harness</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:43:06 +0000</pubDate>
      <link>https://dev.to/himanshu_748/bugs-are-innocent-until-reproduced-building-verdict-an-evidence-first-agent-harness-50lf</link>
      <guid>https://dev.to/himanshu_748/bugs-are-innocent-until-reproduced-building-verdict-an-evidence-first-agent-harness-50lf</guid>
      <description>&lt;p&gt;Most flaky bug reports end in one of two places: "cannot reproduce" or a patch nobody can prove fixed the problem.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/himanshu748/verdict" rel="noopener noreferrer"&gt;Verdict&lt;/a&gt; around a stricter idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bugs are innocent until reproduced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verdict turns a GitHub issue into a bounded investigation. It runs an approved command repeatedly under approved conditions, keeps every observation and refuses to claim a reproduction unless the evidence crosses a deterministic threshold.&lt;/p&gt;

&lt;p&gt;This is not an autonomous patch generator. It is an evidence-producing agent harness for the difficult step that comes before a patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another bug investigation tool?
&lt;/h2&gt;

&lt;p&gt;An LLM can read a stack trace and propose a plausible explanation quickly. Plausible is not the same as reproduced.&lt;/p&gt;

&lt;p&gt;For an intermittent failure, the questions that matter are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which condition actually triggers it?&lt;/li&gt;
&lt;li&gt;How often does it fail under that condition?&lt;/li&gt;
&lt;li&gt;What happens under a contrasting control?&lt;/li&gt;
&lt;li&gt;Which repository range does the evidence support?&lt;/li&gt;
&lt;li&gt;What regression test would prevent the same failure from returning?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verdict treats those as an experiment rather than a conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-act investigation
&lt;/h2&gt;

&lt;p&gt;Verdict uses three bounded subagents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub issue
    |
    v
Hunter: find the trigger
    |
    v
Surgeon: localize the change
    |
    v
Insurance: keep it fixed
    |
    v
Maintainer review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hunter
&lt;/h3&gt;

&lt;p&gt;Hunter searches only the condition matrix and command budget approved by the maintainer. Successful, failed, partial and unresolved runs all stay in the evidence ledger. An inconvenient result cannot disappear just because it weakens the story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surgeon
&lt;/h3&gt;

&lt;p&gt;Surgeon narrows the reproduced condition to the smallest suspect range the records support. Static inspection stays visibly different from a proven execution boundary. Surgeon does not author a patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insurance
&lt;/h3&gt;

&lt;p&gt;Insurance converts the reproduction into a regression plan: the test name, fixture, failing assertion and publication manifest. A draft pull request can only be created through a workflow the maintainer explicitly approves.&lt;/p&gt;

&lt;p&gt;Each act is allowed to claim less than the act before it. None can talk the deterministic reducer into a stronger verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reproduction is a record, not a screenshot
&lt;/h2&gt;

&lt;p&gt;Verdict reproduced &lt;a href="https://github.com/truefoundry/trueforge/issues/417" rel="noopener noreferrer"&gt;TrueForge issue #417&lt;/a&gt;, where snapshot registration can wait indefinitely when an upstream request never resolves.&lt;/p&gt;

&lt;p&gt;The pinned runtime used &lt;code&gt;@truefoundry/trueforge-core@0.1.4#DaytonaSandboxProvider&lt;/code&gt; and ran two conditions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daytona-stalled-endpoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 of 10 runs matched, &lt;code&gt;REPRODUCTION_PINNED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daytona-responsive-endpoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 of 10 matched, &lt;code&gt;NOT_REPRODUCED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The control is the important half. A condition that fails every time next to one that never fails is stronger evidence than twenty failures with no contrast.&lt;/p&gt;

&lt;p&gt;Anyone can recompute the record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; @verdict/agent verify:runtime-evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPRODUCED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stalledRuns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responsiveControls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@truefoundry/trueforge-core@0.1.4#DaytonaSandboxProvider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonicalSha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a8bb5dd22e083782bd7782fccb0a1343b59fc77ea8525b6358fecc9b5b8baffa"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evidence binds the observations to the TrueForge session, Hunter thread, repository commit, npm provenance commit and shared source blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploration and proof are different systems
&lt;/h2&gt;

&lt;p&gt;The model gathers candidate observations. It does not decide what those observations prove.&lt;/p&gt;

&lt;p&gt;Verdict's evidence contract is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Issue text and repository content begin as untrusted input.&lt;/li&gt;
&lt;li&gt;The investigation may use only approved commands, knobs and budgets.&lt;/li&gt;
&lt;li&gt;Every accepted observation must match the evidence schema.&lt;/li&gt;
&lt;li&gt;Pure reducers decide which claim the records support.&lt;/li&gt;
&lt;li&gt;Missing or conflicting evidence produces an honest partial result.&lt;/li&gt;
&lt;li&gt;A maintainer controls the only public write.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same records always produce the same verdict. That is the boundary between an agent exploring a problem and a system making a claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is live and what is a fixture
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://verdict-steel.vercel.app/case/trueforge-417" rel="noopener noreferrer"&gt;recorded case&lt;/a&gt; renders the executed artifact. It includes both conditions, all twenty runs and the recomputable hash.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://verdict-steel.vercel.app/case/demo" rel="noopener noreferrer"&gt;interactive workspace&lt;/a&gt; is a conceptual fixture. Every generated value is labelled. It is not quietly presented as live runtime evidence.&lt;/p&gt;

&lt;p&gt;That distinction matters for a product whose entire argument is that a claim needs a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval remains a maintainer decision
&lt;/h2&gt;

&lt;p&gt;Verdict also exercised its publication boundary against real GitHub. After explicit approval, a nonce-bound workflow ran, verified the external reproduction reference and created a draft pull request in Verdict's repository. The upstream TrueForge repository remained read-only.&lt;/p&gt;

&lt;p&gt;The workflow proof says &lt;code&gt;runtimeReproducedByThisWorkflow: false&lt;/code&gt;. That is deliberate. The provider run reproduced the bug. GitHub Actions verified the harness and published the independently checkable proof. Combining those into one vague "verified" flag would erase the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qodo reviewed the claims as well as the code
&lt;/h2&gt;

&lt;p&gt;Every substantive change went through a pull request reviewed by Qodo before merge.&lt;/p&gt;

&lt;p&gt;The most useful findings were not dramatic crashes. They were mismatches between the implementation and what the project claimed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A landing card still described the real reproduction as simulated.&lt;/li&gt;
&lt;li&gt;CI trusted the recorded verdict instead of recomputing its hash.&lt;/li&gt;
&lt;li&gt;README copy claimed coverage on every push while the workflow covered &lt;code&gt;main&lt;/code&gt; and pull requests.&lt;/li&gt;
&lt;li&gt;A malformed CSS selector silently failed after a cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those reviews fit the product philosophy perfectly: do not ship a stronger claim than the evidence supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current verification
&lt;/h2&gt;

&lt;p&gt;The repository runs the same gate locally and in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm lint
pnpm typecheck
pnpm &lt;span class="nb"&gt;test
&lt;/span&gt;pnpm build
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; @verdict/agent verify:runtime-evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current suite contains 220 tests across the agent, protocol and web packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Verdict
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://verdict-steel.vercel.app" rel="noopener noreferrer"&gt;Live product&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://verdict-steel.vercel.app/case/trueforge-417" rel="noopener noreferrer"&gt;Executed TrueForge #417 record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://verdict-steel.vercel.app/case/demo" rel="noopener noreferrer"&gt;Interactive case workspace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/himanshu748/verdict" rel="noopener noreferrer"&gt;Source code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verdict is open source under the MIT licence and was built for the WeMakeDevs x TrueFoundry Agent Harness Hackathon.&lt;/p&gt;

&lt;p&gt;The goal is not to make an agent sound certain. The goal is to make certainty inspectable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your dog's camera roll is a wellness history. BarkPass makes it speak.</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:36:46 +0000</pubDate>
      <link>https://dev.to/himanshu_748/your-dogs-camera-roll-is-a-wellness-history-barkpass-makes-it-speak-1m5j</link>
      <guid>https://dev.to/himanshu_748/your-dogs-camera-roll-is-a-wellness-history-barkpass-makes-it-speak-1m5j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dog owners take hundreds of photos, then rely on memory for the question that matters: &lt;strong&gt;has anything changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yesterday's posture, last week's energy, that unusually restless look. Each clue is visible for a moment, then buried between screenshots and receipts. A single photo is a memory. A sequence of photos can become a wellness history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BarkPass makes that history queryable, audible and portable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One daily photo becomes a structured visual observation, a short voice update spoken as the dog, a grounded history the owner can question, and an optional on-chain pet passport. BarkPass describes what is visible. It never diagnoses a condition or replaces a veterinarian.&lt;/p&gt;

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

&lt;p&gt;BarkPass is a responsive wellness companion for any dog. Its product loop is &lt;strong&gt;notice, hear, remember, carry&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a profile for your dog.&lt;/li&gt;
&lt;li&gt;Add today's photo or a short video.&lt;/li&gt;
&lt;li&gt;Gemini returns a structured read of visible mood, energy, posture and flags.&lt;/li&gt;
&lt;li&gt;ElevenLabs turns that result into a short first-person voice note.&lt;/li&gt;
&lt;li&gt;Snowflake stores the check-in and grounds plain-language trend answers in the rows that actually exist.&lt;/li&gt;
&lt;li&gt;Solana turns the dog's identity details into a portable, independently verifiable devnet passport.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bruno is the editorial example on the landing page, not a hardcoded user. Every new visitor receives a random dog ID. That ID scopes the profile, check-ins, Snowflake history queries and Solana metadata. Start at &lt;code&gt;/app&lt;/code&gt; and BarkPass is about your dog, not mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try BarkPass
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Create a BarkPass for your dog:&lt;/strong&gt; &lt;a href="https://barkpass-dog-days.vercel.app/app" rel="noopener noreferrer"&gt;https://barkpass-dog-days.vercel.app/app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Explore Bruno's optional sample story:&lt;/strong&gt; &lt;a href="https://barkpass-dog-days.vercel.app/app?sample=1" rel="noopener noreferrer"&gt;https://barkpass-dog-days.vercel.app/app?sample=1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Read the source:&lt;/strong&gt; &lt;a href="https://github.com/himanshu748/barkpass-dog-days" rel="noopener noreferrer"&gt;https://github.com/himanshu748/barkpass-dog-days&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clean app route begins with onboarding. Add a real dog profile, choose a photo, inspect the structured observation, play the voice note, ask a history question and open the passport section.&lt;/p&gt;

&lt;p&gt;The Bruno route is only an optional sample story. A history product needs several days before its most interesting screen makes sense, so the sample provides seven check-ins immediately. It is not a separate build or a hidden judging route.&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%2Frvo16nhn9u494u7k7eav.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%2Frvo16nhn9u494u7k7eav.png" alt="BarkPass personalized daily check-in for Milo with all four provider routes live, plus visible mood, energy, posture and voice results" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The product flow
&lt;/h2&gt;

&lt;p&gt;The first check-in begins in the browser. BarkPass downsizes an image to a maximum edge of 1,600 pixels, or extracts one representative frame from a short video. This reduces upload time and keeps the provider payload bounded. The prepared JPEG is sent only after the owner chooses the file.&lt;/p&gt;

&lt;p&gt;Gemini returns a structured observation. BarkPass saves the normalized result as a check-in, then uses the same result to prepare the dog's voice note. Each additional check-in extends the dog's history. The owner can ask a plain-language question such as "Has Luna's energy changed this week?" and receive an answer calculated from Luna's queried Snowflake rows.&lt;/p&gt;

&lt;p&gt;The passport is a separate, optional action. It carries identity and care details, not the owner's private image history. Minting returns public Solana Explorer proof. A shelter tip is prepared as an unsigned devnet transaction and still requires the owner to approve it in Phantom.&lt;/p&gt;

&lt;p&gt;The product stays useful even when one provider is unavailable. Local profile and check-in history continue to work. Every non-provider result is labelled in the interface so sample data is never presented as live sponsor output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, end to end
&lt;/h2&gt;

&lt;p&gt;The four integrations are stages of one pipeline, not four logos attached to a landing page.&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%2Fh7u2l4kif0homjexsp3y.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%2Fh7u2l4kif0homjexsp3y.jpg" alt="BarkPass vertical architecture map: a private owner-initiated photo flows through server-side Gemini, ElevenLabs, Snowflake and Solana jobs" width="800" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram is a static 1,000 × 1,200 portrait image with large type, so it stays readable at DEV's article width without depending on Mermaid support.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five layers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;What crosses the boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React client&lt;/td&gt;
&lt;td&gt;Dog profile state, media preparation, check-in UI, audio playback, Phantom connection and labelled local resilience&lt;/td&gt;
&lt;td&gt;A prepared JPEG, normalized observation records, dog identity fields and owner-approved wallet actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel Functions&lt;/td&gt;
&lt;td&gt;Validation, provider orchestration, normalization and credential isolation&lt;/td&gt;
&lt;td&gt;Only the minimum provider-specific payload; no provider secret is returned to the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI + ElevenLabs&lt;/td&gt;
&lt;td&gt;Convert one selected frame into structured visible signals, then convert a bounded sentence into audio&lt;/td&gt;
&lt;td&gt;Gemini receives the prepared frame; ElevenLabs receives the derived sentence, never the photo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Durable dog profiles, idempotent check-ins and dog-scoped trend queries&lt;/td&gt;
&lt;td&gt;Text identity fields and structured observations; the original profile/check-in photo is not stored there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana devnet&lt;/td&gt;
&lt;td&gt;Public passport metadata, one-of-one mint proof and unsigned shelter-tip preparation&lt;/td&gt;
&lt;td&gt;Selected passport identity fields and the connected public wallet address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  One check-in request sequence
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepare in the browser.&lt;/strong&gt; A chosen photo is resized to a maximum edge of 1,600 pixels and encoded as JPEG at 0.84 quality. For a short video, BarkPass seeks to an early representative frame and sends that frame rather than the whole clip. Nothing leaves the browser before the owner chooses media.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe through &lt;code&gt;POST /api/analyze&lt;/code&gt;.&lt;/strong&gt; The function checks the method, media type and payload size before calling &lt;code&gt;gemini-2.5-flash&lt;/code&gt;. Gemini is constrained to a JSON schema. The function parses and normalizes the response again, clamps energy and confidence to their allowed ranges, caps visible flags, and derives the bounded voice sentence on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speak through &lt;code&gt;POST /api/voice&lt;/code&gt;.&lt;/strong&gt; Only that short sentence is sent to ElevenLabs. The function limits it to 500 characters and returns private-cacheable &lt;code&gt;audio/mpeg&lt;/code&gt;; the browser creates a temporary object URL for playback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember through &lt;code&gt;POST /api/dogs&lt;/code&gt; and &lt;code&gt;POST /api/checkins&lt;/code&gt;.&lt;/strong&gt; Snowflake receives the current random &lt;code&gt;dog_id&lt;/code&gt;, profile fields and the normalized observation. &lt;code&gt;MERGE&lt;/code&gt; makes profile and check-in retries idempotent instead of duplicating history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer through &lt;code&gt;POST /api/query&lt;/code&gt;.&lt;/strong&gt; The function synchronizes at most 30 recent local check-ins, selects only rows for the requested &lt;code&gt;dog_id&lt;/code&gt;, and computes the row count, average, range, direction and most common mood from the returned values. The prose is assembled from those facts; an unrestricted model does not invent the trend answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carry through &lt;code&gt;POST /api/solana/mint&lt;/code&gt;.&lt;/strong&gt; The server validates the current dog, creates a same-origin metadata URL and uses Metaplex to mint a one-of-one &lt;code&gt;BARK&lt;/code&gt; token on devnet. The response returns both the mint and transaction Explorer URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve owner control through &lt;code&gt;POST /api/solana/tip&lt;/code&gt;.&lt;/strong&gt; The server builds a 0.01 SOL devnet transfer with the connected wallet as fee payer, but deliberately does not sign it. The browser hands the serialized transaction to Phantom; only the owner can approve and broadcast it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Identity and data isolation
&lt;/h3&gt;

&lt;p&gt;The same random dog ID is the join key across browser storage, &lt;code&gt;BARKPASS_DOGS&lt;/code&gt;, &lt;code&gt;BARKPASS_CHECKINS&lt;/code&gt;, history queries and passport metadata. It is generated when a real profile is created and preserved when that profile is edited. Replacing the optional Bruno sample clears every sample-only field and creates a fresh ID, so a new owner cannot inherit Bruno's photo, microchip, vaccination or seven-day history.&lt;/p&gt;

&lt;p&gt;Snowflake has two durable entities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BARKPASS_DOGS     dog_id → name, breed, age, microchip, vaccination
BARKPASS_CHECKINS (checkin_id, dog_id) → date, mood, energy,
                    posture, flags, confidence, summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compound check-in key matters: &lt;code&gt;checkin_id&lt;/code&gt; makes retries safe, while &lt;code&gt;dog_id&lt;/code&gt; prevents one dog's retry from overwriting another dog's record. Query parameters are bound rather than concatenated, and history reads always include &lt;code&gt;WHERE dog_id = ?&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust boundaries and failure behavior
&lt;/h3&gt;

&lt;p&gt;The browser is treated as public. Gemini, ElevenLabs, Snowflake and Solana credentials exist only in Vercel's server environment. Functions validate and bound every incoming field before a provider call. Snowflake uses RSA key-pair authentication through a dedicated least-privilege service role, while the Solana authority is limited to devnet. The shelter-tip path is intentionally split: the server can prepare a transaction, but Phantom retains the owner's signing authority.&lt;/p&gt;

&lt;p&gt;Each client adapter also has an explicit resilience boundary. If vision is unavailable, a clearly labelled sample observation keeps the interface demonstrable. If voice fails, device speech can keep the ritual audible. If Snowflake is unavailable, the current browser retains local history and computes the same transparent statistics. On-chain actions never pretend to succeed: live responses return Explorer evidence, verified examples are labelled as examples, and a real tip cannot leave the wallet without Phantom.&lt;/p&gt;

&lt;p&gt;This separation is what lets BarkPass degrade one capability without collapsing the whole daily check-in—and without presenting fallback output as live sponsor output.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Google AI observes without pretending to diagnose
&lt;/h2&gt;

&lt;p&gt;Gemini does not receive a vague "how is this dog feeling?" prompt. The server gives it a narrow role: careful veterinary behavior observer. It may describe only visible signals, must avoid diagnosis and must return JSON matching a strict schema.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;required&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;mood&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;energy_level&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;posture_notes&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;health_flags&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;confidence&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;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;enum&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;anxious&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;relaxed&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;playful&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;alert&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;tired&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;energy_level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&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="na"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;posture_notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;health_flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&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;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&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="na"&gt;maximum&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="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;The endpoint uses Gemini's JSON response mode with temperature &lt;code&gt;0.2&lt;/code&gt;, then normalizes every field again before rendering it. The closed mood vocabulary and numeric bounds make daily observations comparable. A paragraph that changes tone every day would be difficult to chart or query. A stable record is useful.&lt;/p&gt;

&lt;p&gt;The most important product rule is: &lt;strong&gt;describe the frame, do not invent the dog.&lt;/strong&gt; A low confidence score stays visible. A visible flag becomes a reason to look again, not a medical conclusion.&lt;/p&gt;

&lt;p&gt;I ran five licensed dog photographs through the live provider path. Gemini returned four distinct moods and energy values from 4 to 9.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo&lt;/th&gt;
&lt;th&gt;Mood&lt;/th&gt;
&lt;th&gt;Energy&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;API latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Playful&lt;/td&gt;
&lt;td&gt;6/10&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;10.53 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Playful&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;4.30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Anxious&lt;/td&gt;
&lt;td&gt;4/10&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;td&gt;8.38 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Relaxed&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;6.81 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Alert&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;4.68 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average Gemini latency was 6.94 seconds. The test matters because a vision feature that returns "happy dog" for every photograph is only decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ElevenLabs is more than a play button
&lt;/h2&gt;

&lt;p&gt;A score is useful. A sentence is memorable.&lt;/p&gt;

&lt;p&gt;BarkPass turns the normalized observation into one short first-person line. High energy produces a more active suggestion. Low energy produces a quieter one. A visible flag asks the owner to take another look without claiming a diagnosis. That bounded line is sent to ElevenLabs using &lt;code&gt;eleven_flash_v2_5&lt;/code&gt; and returned as a 44.1 kHz, 128 kbps MP3.&lt;/p&gt;

&lt;p&gt;A timed live call produced an 84,889-byte MP3 in 1.55 seconds. The final Production recheck returned HTTP 200 with a playable 52,288-byte MPEG; output size varies with the generated sentence. Combined with the slowest timed Gemini call, the measured provider path was 12.08 seconds, inside the brief's 15-second target.&lt;/p&gt;

&lt;p&gt;If the provider cannot respond, BarkPass can use device speech so the daily ritual does not dead-end. The interface labels that output as a fallback. Device speech is resilience, not evidence of ElevenLabs usage. The provider-backed measurement and playable MP3 are the evidence.&lt;/p&gt;

&lt;p&gt;That distinction matters because BarkPass is not claiming to translate dogs. The voice is an interface for recall. Owners are more likely to remember "I am taking things slowly today" than a row containing &lt;code&gt;energy_level: 4&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Snowflake is the product's memory
&lt;/h2&gt;

&lt;p&gt;One observation is a moment. Several observations become a pattern.&lt;/p&gt;

&lt;p&gt;Profiles are merged into &lt;code&gt;BARKPASS_DOGS&lt;/code&gt;. Check-ins are merged into &lt;code&gt;BARKPASS_CHECKINS&lt;/code&gt; using both &lt;code&gt;checkin_id&lt;/code&gt; and &lt;code&gt;dog_id&lt;/code&gt;. The compound match makes retries idempotent and prevents a repeated save from creating duplicate history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;BARKPASS_CHECKINS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
&lt;span class="k"&gt;USING&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;source&lt;/span&gt;
&lt;span class="k"&gt;ON&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;checkin_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkin_id&lt;/span&gt;
&lt;span class="k"&gt;AND&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;dog_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dog_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;History questions query only the requested dog's rows:&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;checkin_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;energy_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;health_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary_text&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;BARKPASS_CHECKINS&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;dog_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&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;checkin_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned sentence is calculated from those energy values and mood counts. BarkPass reports the row count, average and range beside the answer. It cannot invent a trend that is absent from the query result.&lt;/p&gt;

&lt;p&gt;The deployment uses an X-Small auto-suspending warehouse and a dedicated service user. Key-pair authentication is preferred over a password, and the runtime role is limited to BarkPass's database, schema, tables and warehouse.&lt;/p&gt;

&lt;p&gt;In live Production verification, BarkPass stored Maple and three check-ins with energy values 5, 7 and 8. The API reported an average of 6.7, a range of 5 to 8 and a higher final value. A fresh Snowflake worksheet aggregate independently returned the same row count, average, minimum and maximum.&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%2Fekbopwuxb8l6r3kss61h.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%2Fekbopwuxb8l6r3kss61h.jpg" alt="Fresh Snowflake worksheet aggregate for Maple's three Production check-ins" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snowflake is not an analytics screenshot added after the product. It is why BarkPass can answer &lt;strong&gt;"Has her energy changed?"&lt;/strong&gt; with the supporting numbers beside the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Solana carries the passport
&lt;/h2&gt;

&lt;p&gt;A pet passport is more useful when it is not trapped inside one application's database.&lt;/p&gt;

&lt;p&gt;The mint endpoint validates the dog first, then generates a public metadata URL containing name, breed, age, microchip ID and vaccination date. Metaplex creates a one-of-one NFT on Solana devnet with symbol &lt;code&gt;BARK&lt;/code&gt;, zero royalties and BarkPass's server vault as the mint authority.&lt;/p&gt;

&lt;p&gt;Maple's BarkPass was minted from the public Production route as &lt;a href="https://explorer.solana.com/address/B3FRp9ndjjiVbFwb7MKruabsHicPQQCnL6JdksErEWwa?cluster=devnet" rel="noopener noreferrer"&gt;&lt;code&gt;B3FRp9ndjjiVbFwb7MKruabsHicPQQCnL6JdksErEWwa&lt;/code&gt;&lt;/a&gt;. The &lt;a href="https://explorer.solana.com/tx/5nAjwN21Phz6Deia5uYzGCaRkD5v7xm7cjK5pA3JJWAQciPB357hmeQb5UKpzmzwFSRWbpMKrbpZrdJTnXMfHjWu?cluster=devnet" rel="noopener noreferrer"&gt;mint transaction&lt;/a&gt; finalized without error. Explorer shows the &lt;code&gt;BARK&lt;/code&gt; non-fungible token, Maple's public metadata URL and BarkPass's new funded devnet vault as authority.&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%2F6cfbai26e92zp5cwjhyd.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%2F6cfbai26e92zp5cwjhyd.png" alt="Finalized BarkPass mint transaction on Solana devnet" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The shelter-tip endpoint does something deliberately different. It prepares a 0.01 SOL devnet transfer with the connected wallet as fee payer, but the server does not sign for the owner. Phantom receives the serialized transaction and must explicitly approve it before broadcast.&lt;/p&gt;

&lt;p&gt;Devnet SOL has no monetary value. The proof is the verifiable lifecycle: generate dog-specific metadata, mint once, return Explorer evidence and preserve wallet approval for a transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest engineering decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Making the app about every dog, not Bruno
&lt;/h3&gt;

&lt;p&gt;The landing page needed a memorable dog, but the product could not inherit that dog's identity. Profile creation now generates a random dog ID. Check-ins are stored under that ID in the browser and in Snowflake. Passport metadata is created from the current profile. A contract test specifically asks for Luna's history and fails if Bruno appears anywhere in the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping a four-provider flow understandable
&lt;/h3&gt;

&lt;p&gt;It would have been easy to show four independent buttons. That would satisfy a checklist but create no product. BarkPass instead gives each provider one job in a single narrative: Gemini notices, ElevenLabs gives the moment a voice, Snowflake remembers and Solana lets the passport travel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designing honest failure states
&lt;/h3&gt;

&lt;p&gt;Provider credentials stay in server environments. The public deployment does not expose billable keys to the browser. If a provider is unavailable, BarkPass keeps local history usable and labels the resulting source. It never paints a fallback response as live AI, Snowflake or on-chain activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making a weekend build feel complete
&lt;/h3&gt;

&lt;p&gt;The app supports photo and short-video input, loading, empty, success and error states, keyboard focus, reduced motion, 390-pixel mobile layouts and desktop layouts. The first-run reveal transition was browser-tested after it briefly produced a blank dashboard. The Solana function bundle also needed &lt;code&gt;rpc-websockets&lt;/code&gt; pinned to &lt;code&gt;9.3.8&lt;/code&gt; so Vercel's CommonJS build received a compatible &lt;code&gt;uuid&lt;/code&gt; release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy, safety and reliability boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Profile photos stay in the owner's browser.&lt;/li&gt;
&lt;li&gt;A prepared check-in frame is sent only after the owner selects a file.&lt;/li&gt;
&lt;li&gt;Text profile details and check-ins sync to Snowflake when that route is available.&lt;/li&gt;
&lt;li&gt;Dog IDs scope every profile, query and passport.&lt;/li&gt;
&lt;li&gt;Local profiles and history remain usable when a provider is unavailable.&lt;/li&gt;
&lt;li&gt;Fallbacks and sample data are explicitly labelled.&lt;/li&gt;
&lt;li&gt;Passport minting uses Solana devnet.&lt;/li&gt;
&lt;li&gt;Shelter tips require Phantom approval.&lt;/li&gt;
&lt;li&gt;BarkPass is a wellness companion, not veterinary advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public client bundle is intentionally secret-free. All four real provider paths now run through the public Production deployment's same-origin server functions, while the repository contains the complete server implementation and reproducible verification record. No provider key, Snowflake private key or Solana vault secret is shipped to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;The Node test suite covers provider contracts and the isolation boundaries that would be easiest to get wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini JSON normalization and the generated voice line&lt;/li&gt;
&lt;li&gt;playable ElevenLabs audio bytes&lt;/li&gt;
&lt;li&gt;incomplete Snowflake check-in rejection&lt;/li&gt;
&lt;li&gt;Snowflake key-pair authentication without password exposure&lt;/li&gt;
&lt;li&gt;integration status with key-pair credentials&lt;/li&gt;
&lt;li&gt;dog profile validation&lt;/li&gt;
&lt;li&gt;dog-specific history with no Bruno inheritance&lt;/li&gt;
&lt;li&gt;dynamic Solana metadata for the requested dog&lt;/li&gt;
&lt;li&gt;validation before any mint creates chain state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the same checks locally:&lt;br&gt;
&lt;/p&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;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All thirteen contract tests pass. The production Vite build passes. I also verified the first-run flow, sample-to-personal profile isolation, the optional seven-day sample, responsive behavior at 390 pixels, public metadata, the finalized devnet mint, a playable ElevenLabs MP3, the five-photo Gemini set and Snowflake's independent aggregate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;Clone the repository and copy the environment contract:&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/barkpass-dog-days.git
&lt;span class="nb"&gt;cd &lt;/span&gt;barkpass-dog-days
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env.local
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface works locally with labelled resilience paths. To exercise live providers, configure the values documented in &lt;code&gt;.env.example&lt;/code&gt; and point &lt;code&gt;VITE_API_BASE_URL&lt;/code&gt; at a running functions deployment. The core server variables are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GEMINI_API_KEY
ELEVENLABS_API_KEY
SNOWFLAKE_ACCOUNT
SNOWFLAKE_USER
SNOWFLAKE_PRIVATE_KEY_BASE64
SNOWFLAKE_WAREHOUSE
SNOWFLAKE_DATABASE
SNOWFLAKE_SCHEMA
SOLANA_RPC
SOLANA_VAULT_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real values belong only in local or deployment secrets. They must never be committed.&lt;/p&gt;

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

&lt;p&gt;I am entering BarkPass for &lt;strong&gt;Overall Winner, Best Use of Google AI, Best Use of ElevenLabs, Best Use of Snowflake and Best Use of Solana&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The theme connection is direct: the whole product begins with a dog owner's daily photo. The creative choice is to turn that familiar habit into something the owner can hear, question and carry. The technical execution is the connected pipeline and its source boundaries, not the number of APIs used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;p&gt;BarkPass is a weekend prototype, not a clinical product. Visual observations can be wrong, which is why confidence stays visible and the language remains non-diagnostic. The Production app has all four provider routes configured, while labelled local resilience keeps a check-in usable during a provider outage. The shelter-tip transaction has been prepared successfully, but final broadcast still requires an owner's Phantom approval. There is no account system or multi-device sync yet.&lt;/p&gt;

&lt;p&gt;These are explicit boundaries, not hidden promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;The next version would add authenticated multi-device profiles, owner-controlled sharing with carers or veterinarians, reminders, exportable wellness summaries and rate-limited public provider routes. I would also evaluate Gemini against a larger, more diverse photo set and make the passport updateable through an owner-controlled authority.&lt;/p&gt;

&lt;p&gt;Most pet apps begin by asking owners to become better record keepers. BarkPass begins with something they already do: take a photo.&lt;/p&gt;

&lt;p&gt;That small habit becomes a story they can hear, question and carry with their dog.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Bought a ₹6 Share and Learned the Hard Way: Building FinEd Saathi in 10 Days</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 15 Aug 2026 04:47:37 +0000</pubDate>
      <link>https://dev.to/himanshu_748/i-bought-a-6-share-and-learned-the-hard-way-building-fined-saathi-in-10-days-1980</link>
      <guid>https://dev.to/himanshu_748/i-bought-a-6-share-and-learned-the-hard-way-building-fined-saathi-in-10-days-1980</guid>
      <description>&lt;p&gt;I once bought a share at about ₹6 without understanding what the complete transaction would cost. I later sold it around the same price and was surprised to see a loss of roughly ₹50 somewhere in my account. I did not know whether that number came from the contract note, ledger, available funds or P&amp;amp;L view. The important lesson was not that every small trade has the same charge. It was that I had acted without understanding brokerage, taxes or where a number in a broker app actually came from.&lt;/p&gt;

&lt;p&gt;That experience became the starting point for FinEd Saathi, the voice-first financial literacy tutor I built during &lt;a href="https://github.com/murf-ai/voice-for-bharat-challenge-2026/blob/main/challenges/Day%2010%20Task.md" rel="noopener noreferrer"&gt;10 Days of Voice Agents - VoiceForBharat Edition&lt;/a&gt;. I chose the Financial Services track because I wanted to solve a problem I had felt myself. The challenge listed Financial Services and Learning &amp;amp; Literacy as two separate tracks. I formally stayed in Financial Services, but I deliberately merged finance with education in the product because access without understanding was the problem I wanted to solve. The tutor is for beginners who want Indian market concepts explained in a patient conversation before they put real money at risk. The public code is in the &lt;a href="https://github.com/himanshu748/fin-ed" rel="noopener noreferrer"&gt;FinEd Saathi repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to solve
&lt;/h2&gt;

&lt;p&gt;Financial education often assumes that a learner already knows the vocabulary. A person who is still asking what an ETF is may be sent to a fee schedule, a tax circular or a dense product page. Even correct information can be hard to use when several unfamiliar concepts arrive at once.&lt;/p&gt;

&lt;p&gt;I wanted the product to begin where the learner is. FinEd can explain stocks, mutual funds and SIPs, ETFs, gold, F&amp;amp;O, IPOs and bonds. It can also unpack a confusing charge without inventing a reason. In my ₹6 share story, the agent treats the price profit or loss as zero until I show where the separate loss appeared, such as a contract note, ledger or P&amp;amp;L view. That is more useful than confidently guessing.&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%2Fsmmrc2sebsajbvhpndgj.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%2Fsmmrc2sebsajbvhpndgj.png" alt="FinEd Saathi landing page with learning modes and a voice call to action" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The landing page lets a beginner choose a topic before starting a voice lesson.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why voice changed the product
&lt;/h2&gt;

&lt;p&gt;Voice makes the lesson feel like a conversation instead of an exam. A learner can ask a short question, interrupt or switch between English, Hindi and a code-mixed register. Deepgram Nova-3 handles multilingual speech recognition. Gemini handles the teaching conversation and tools. LiveKit carries the real-time session.&lt;/p&gt;

&lt;p&gt;Murf Falcon is the fastest TTS API. I use &lt;a href="https://murf.ai/api/docs/text-to-speech-models/falcon-2" rel="noopener noreferrer"&gt;Murf Falcon 2&lt;/a&gt; with Nikhil as the FinEd voice because his Indian conversational delivery fits a patient tutor. Murf documents Falcon 2 as a real-time speech model with roughly 100 ms time to first audio. The low-latency path matters because a financial explanation quickly feels unnatural when every turn contains a long pause.&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%2Fzmo3f3app25vmmym00en.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%2Fzmo3f3app25vmmym00en.png" alt="FinEd voice workspace showing an ETF lesson, live agent state and topic controls" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The voice workspace shows what the agent heard, which specialist is active and what the learner can do next.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How FinEd Saathi works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffr9w4f4yrjavdxd8m13h.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%2Ffr9w4f4yrjavdxd8m13h.png" alt="Architecture diagram showing browser audio flowing through LiveKit, Deepgram, Gemini and Murf with safe optional systems" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser sends audio through LiveKit to a Python worker. Deepgram transcribes the speech, Gemini produces a bounded response and Murf Falcon 2 streams the spoken answer. FinEd owns ordinary lessons. TaxEd is a separate specialist reached only after permission. Optional market data, memory, paper trading, human help, outbound telephony and analytics sit outside the essential voice path so missing optional credentials do not break the core tutor.&lt;/p&gt;

&lt;p&gt;The main voice session is deliberately compact:&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;fined_tts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;murf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nikhil&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conversational&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;falcon-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-IN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Initial locale, selected again before each reply stream.
&lt;/span&gt;    &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SentenceTokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_sentence_len&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;text_pacing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fined_locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FinEdTTSLocaleController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fined_tts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;
    &lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fined_stt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fined_llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fined_tts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;turn_detection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TurnDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;preemptive_generation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before LiveKit opens each FinEd speech stream, the controller peeks at the actual reply. Devanagari Hindi selects &lt;code&gt;hi-IN&lt;/code&gt;; English and Latin-script code-mixed replies select &lt;code&gt;en-IN&lt;/code&gt;. That keeps Nikhil fixed while applying a supported locale before Murf snapshots the stream options. TaxEd creates a separate Murf TTS instance using Anusha and a server-normalized &lt;code&gt;en-IN&lt;/code&gt;, &lt;code&gt;hi-IN&lt;/code&gt; or &lt;code&gt;hi-LATN&lt;/code&gt; locale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the learner can actually do
&lt;/h2&gt;

&lt;p&gt;A learner can ask for definitions, compare concepts or request a read-only market quote when optional Angel One access is configured. Quote tools can search instruments, return current prices and fetch historical daily closes. They cannot read holdings, positions or account information. If the broker token is missing or expired, the agent says that live data is unavailable instead of guessing a price.&lt;/p&gt;

&lt;p&gt;The paper portfolio starts with ₹1,00,000 in virtual cash. It supports simulated NSE EQ cash equity or ETF delivery orders based on a fresh quote. The browser prepares a draft then requires explicit confirmation of that same unexpired draft. The portfolio, virtual cash and fill history stay in browser storage. No real broker order API is called.&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%2Fxbeu3ml35fg38ftvvdoa.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%2Fxbeu3ml35fg38ftvvdoa.png" alt="Paper trading dashboard with virtual cash, no holdings and a clear simulation warning" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Paper trading gives the learner a safe place to practise without connecting a real trading account.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sessions behave more like a familiar chat product. Meaningful transcripts are stored under local session identifiers in the browser so the learner can switch conversations. Caller memory is separate, private and consent-gated. It saves only learning preferences after a fresh yes. Credentials, government identifiers, account numbers, holdings, income and bank details are excluded.&lt;/p&gt;

&lt;p&gt;The analytics page reports anonymous totals such as call count, speaking time, committed handoffs and successful calls. It stores no audio, transcript, utterance text, caller identity or phone number.&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%2Fawwptoeu8qu5tfsewcwc.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%2Fawwptoeu8qu5tfsewcwc.png" alt="Anonymous call analytics dashboard with total calls, speaking time, handoffs and success" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Analytics proves whether the experience completed useful actions without turning learners into a surveillance dataset.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built TaxEd as a separate specialist
&lt;/h2&gt;

&lt;p&gt;Investment tax questions need a stricter evidence boundary than a general concept lesson. FinEd first asks whether the learner wants to connect. Only a fresh explicit yes transfers the tax question to TaxEd. Returning to FinEd also requires permission.&lt;/p&gt;

&lt;p&gt;TaxEd speaks with Anusha, a Murf Indian voice whose &lt;a href="https://murf.ai/api/docs/voices-styles/voice-library" rel="noopener noreferrer"&gt;official voice-library entry&lt;/a&gt; supports English and Hindi. It searches a packaged registry of official Indian tax rules, states the relevant applicability date and links the source. A rule must be current for the requested date. When a current packaged source is missing, uncertain or past its review date, TaxEd abstains. It does not calculate a person's final liability, file a return or help evade tax.&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%2Fst3kuowrztbec6987xsa.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%2Fst3kuowrztbec6987xsa.png" alt="TaxEd handoff showing Anusha, the specialist status and a sourced investment-tax explanation" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The handoff is visible, consented and evidence-first.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety was part of the architecture
&lt;/h2&gt;

&lt;p&gt;FinEd is education, not investment advice. Deterministic guardrails run before provider inference for real trading, personalized recommendations, guaranteed outcomes, unsafe F&amp;amp;O calls, credential requests and tax evasion. A request to place a real order is refused even when it also mentions paper trading.&lt;/p&gt;

&lt;p&gt;The system never asks for a broker password, PIN, OTP, PAN, Aadhaar, bank detail or full account number. F&amp;amp;O mode teaches mechanics, payoff examples and risk only. Human-help requests use a short redacted summary and require fresh consent before storage. Outbound calling is an operator-only optional path that also requires explicit consent. Missing telephony configuration leaves browser voice available and makes the call fail closed.&lt;/p&gt;

&lt;p&gt;I documented the deterministic evidence in &lt;a href="https://github.com/himanshu748/fin-ed/blob/main/RED_TEAM.md" rel="noopener noreferrer"&gt;&lt;code&gt;RED_TEAM.md&lt;/code&gt;&lt;/a&gt;, including broker identifier redaction during a TaxEd handoff, refusal before model inference and abstention on an unverified tax rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest parts
&lt;/h2&gt;

&lt;p&gt;The hardest work was not drawing the interface. It was making state transitions reliable. A handoff that only changes a badge is not a handoff. I had to keep the active specialist, the TTS voice, the transferred question and the return path aligned. I also had to stop repeated permission loops when a clear yes had already been given.&lt;/p&gt;

&lt;p&gt;Multilingual speech exposed a similar issue. A multilingual voice identity is not enough when the synthesis locale is fixed. FinEd now routes Nikhil to &lt;code&gt;hi-IN&lt;/code&gt; for Devanagari Hindi and &lt;code&gt;en-IN&lt;/code&gt; for English before audio begins. TaxEd keeps Anusha while a server-normalized &lt;code&gt;en-IN&lt;/code&gt;, &lt;code&gt;hi-IN&lt;/code&gt; or &lt;code&gt;hi-LATN&lt;/code&gt; locale follows the handoff language. The browser cannot choose arbitrary synthesis settings.&lt;/p&gt;

&lt;p&gt;Live market data taught me to design for absence. Broker access tokens expire. The correct fallback is not a stale or invented price. The agent now says the quote is unavailable while concept lessons, the empty paper dashboard and other safe features continue.&lt;/p&gt;

&lt;p&gt;I also learned that proof assets are part of product quality. A blurred screenshot can hide the very behavior the article claims to demonstrate. I recaptured the specialist handoff and analytics views so the state, source and privacy boundary are readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;You need Python 3.10 through 3.14, &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;, Node.js and the repository's pinned pnpm 9 release. Clone the &lt;a href="https://github.com/himanshu748/fin-ed" rel="noopener noreferrer"&gt;public repository&lt;/a&gt; then create local environment files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;backend/.env.example backend/.env.local
&lt;span class="nb"&gt;cp &lt;/span&gt;frontend/.env.example frontend/.env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these required backend values to &lt;code&gt;backend/.env.local&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;LIVEKIT_URL
LIVEKIT_API_KEY
LIVEKIT_API_SECRET
MURF_API_KEY
DEEPGRAM_API_KEY
GOOGLE_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the same LiveKit project values in &lt;code&gt;frontend/.env.local&lt;/code&gt; and keep the worker name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVEKIT_URL
LIVEKIT_API_KEY
LIVEKIT_API_SECRET
AGENT_NAME=my-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep real values only in &lt;code&gt;.env.local&lt;/code&gt;. Never commit them. Install the backend and its local voice models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run &lt;span class="nt"&gt;-m&lt;/span&gt; livekit.agents download-files
uv run dotenv &lt;span class="nt"&gt;-f&lt;/span&gt; .env.local run &lt;span class="nt"&gt;--&lt;/span&gt; python src/agent.py start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a fresh shell, install and run the frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev &lt;span class="nt"&gt;--port&lt;/span&gt; 3001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:3001&lt;/code&gt;, select a learning mode, choose &lt;strong&gt;Talk to FinEd Saathi&lt;/strong&gt; and allow microphone access and browser audio playback. Ask "What is an ETF?" Then ask how an equity ETF is taxed and say "Yes, connect me to TaxEd" when FinEd requests permission.&lt;/p&gt;

&lt;p&gt;Common fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection fails: confirm frontend and worker use the same LiveKit project and &lt;code&gt;AGENT_NAME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Voice is silent: confirm &lt;code&gt;MURF_API_KEY&lt;/code&gt;, microphone permission and browser audio playback.&lt;/li&gt;
&lt;li&gt;Live quote unavailable: refresh the optional Angel One access token. Do not guess a quote.&lt;/li&gt;
&lt;li&gt;TaxEd abstains: verify the packaged official rule is current for the requested date.&lt;/li&gt;
&lt;li&gt;Outbound call fails: verify explicit consent, the stored LiveKit SIP trunk and Twilio termination setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angel One, Twilio, the local knowledge index and outbound calling are optional. A normal voice lesson works without them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;The backend has a deterministic suite for guardrails, tools, handoffs, tax rules, memory, analytics and outbound consent. The frontend contracts cover the public interface, browser paper portfolio, token route and documentation. I keep the provider-backed evaluation separate because it requires valid external credentials and inference access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend
uv run pytest &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--ignore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tests/test_agent.py
uv run ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
uv run ruff format &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
node &lt;span class="nt"&gt;--test&lt;/span&gt; tests/&lt;span class="k"&gt;*&lt;/span&gt;.test.mjs
pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
pnpm format:check
pnpm build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The red-team record states exactly which deterministic tests support each claim. I prefer that to describing a manual demo as proof of every failure path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;I would make the source registry easier to update with a review workflow, add more broker-independent historical lessons and test interruption latency across a wider set of Indian network conditions. I would also add an export that lets a learner keep a private summary of concepts they understood without exporting a transcript.&lt;/p&gt;

&lt;p&gt;Ten days turned one confusing small trade into a product I personally find smooth to use. More importantly, it changed how I think about a finance assistant. The best version is not the one that sounds most confident. It is the one that teaches clearly, proves its sources, asks permission at boundaries and knows when to stop.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>voiceai</category>
      <category>fintech</category>
      <category>india</category>
    </item>
    <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>
  </channel>
</rss>
