<?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>INKSHIFT: cross out a table, keep the booking</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:03:36 +0000</pubDate>
      <link>https://dev.to/himanshu_748/inkshift-cross-out-a-table-keep-the-booking-344i</link>
      <guid>https://dev.to/himanshu_748/inkshift-cross-out-a-table-keep-the-booking-344i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Someone has booked Ticket to Ride at Table B. Then Table B becomes unavailable.&lt;/p&gt;

&lt;p&gt;Moving the game to Table C sounds easy. But there is already a person attached to that plan, and their booking needs to survive the edit.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://inkshift.vercel.app" rel="noopener noreferrer"&gt;INKSHIFT&lt;/a&gt; for this kind of change. It turns a plan for a games night, workshop or club meetup into a shared signup page. Upload a photo or type the plan, check the sessions, and send the invite link. Guests can book a place without creating an account.&lt;/p&gt;

&lt;p&gt;When the plan changes, you review the proposed edits against the people who have already joined. You can correct the reading, check the affected bookings and approve the move. Guests keep their places at the new location.&lt;/p&gt;

&lt;p&gt;Sanity gives the gathering continuity: Content Lake stores the linked sessions and registrations, Workflows records each plan review, and App SDK subscribes to the shared schedule. The booking belongs to a session whose location can change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://inkshift.vercel.app" rel="noopener noreferrer"&gt;Open INKSHIFT&lt;/a&gt; · &lt;a href="https://youtu.be/xM5eC-q7t_0" rel="noopener noreferrer"&gt;Watch the walkthrough&lt;/a&gt; · &lt;a href="https://github.com/himanshu748/inkshift" rel="noopener noreferrer"&gt;Source code&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The 43-second walkthrough follows a prepared games-night plan through a table change:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/xM5eC-q7t_0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can try the same flow yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Try a sample&lt;/strong&gt; and open its participant invite.&lt;/li&gt;
&lt;li&gt;Join Ticket to Ride, then return to the organiser workspace.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Use the crossed-out example&lt;/strong&gt; and review the proposed move to Table C.&lt;/li&gt;
&lt;li&gt;Approve it, then reopen the participant page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the organiser's review with a booking already in place:&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%2Fs7nlpywcxff5edjw8nlx.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%2Fs7nlpywcxff5edjw8nlx.jpg" alt="Organiser review showing Ticket to Ride moving from Table B to Table C, with one registration preserved" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The review identifies the move and the registration that stays with it. INKSHIFT checks that Table C has enough seats and is available for the full session before allowing approval.&lt;/p&gt;

&lt;p&gt;After approval, the guest's existing booking appears under &lt;strong&gt;Your places&lt;/strong&gt; at Table C:&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%2Fapm8wzcowjnd63d1oij6.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%2Fapm8wzcowjnd63d1oij6.jpg" alt="Participant page showing the existing Ticket to Ride booking at Table C" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They do not need to sign up again.&lt;/p&gt;

&lt;p&gt;The video and screenshots use labelled prepared samples with fixed readings. The screenshots show a saved demo registration and an applied review. Separate photo-reader checks used rendered typed sheets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrub back through the paper
&lt;/h3&gt;

&lt;p&gt;Every approved edit becomes a version. Below the live plan there is a &lt;strong&gt;Paper time machine&lt;/strong&gt;: a slider over every version of the paper, with the sheet on the left and the tables on the right. Move from version 1 to version 2 and the Ticket to Ride card slides from Table B to Table C, carrying Ada's token with it. Move back and it slides home.&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%2F799iyg5z2ii73ohp6m9c.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%2F799iyg5z2ii73ohp6m9c.png" alt="Paper time machine between version 1 and 2, with Ticket to Ride moving into Table C and its booking token travelling with it" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each version shows when it was applied, what changed ("Ticket to Ride: Table B → Table C"), how many bookings were kept and the state of its Workflows run. Readings I discarded appear as a faded note, never as a version.&lt;/p&gt;

&lt;p&gt;Version 1 in the sample needs no upload: choose &lt;strong&gt;Try a sample&lt;/strong&gt;, join as a guest, apply the crossed-out example and the time machine has two versions. The rename example adds a third.&lt;/p&gt;

&lt;p&gt;It is built from records, not an animation of a guess. Plans only change when a proposal is applied, so each applied proposal's stored preview is exactly the plan it produced. The one gap was the plan before the first edit: nothing stored it. INKSHIFT now records a &lt;code&gt;planBefore&lt;/code&gt; snapshot on the proposal at apply time. Older sample gatherings that predate the snapshot rebuild version 1 from the prepared sample definition, and the time machine labels that version &lt;strong&gt;Reconstructed&lt;/strong&gt;. Bookings are placed per version from their &lt;code&gt;createdAt&lt;/code&gt; and &lt;code&gt;cancelledAt&lt;/code&gt; times; they always point at the same session ID, so nothing has to be guessed about where a person went.&lt;/p&gt;

&lt;p&gt;The timeline, photos and booking initials come from an organiser-only server route. A request without the organiser cookie gets a 403, and the App SDK public schedule still carries only sessions and counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Sanity keeps the booking attached
&lt;/h3&gt;

&lt;p&gt;In Content Lake, spaces, sessions and registrations have separate identities. A registration points to a session; the session points to its space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Registration → Ticket to Ride → Table B
                       ↓ move approved
Registration → Ticket to Ride → Table C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving Ticket to Ride changes its space reference. Its session ID stays the same, so the registrations still belong to it. You can inspect these relationships in the &lt;a href="https://github.com/himanshu748/inkshift/blob/main/sanity/schemaTypes.ts" rel="noopener noreferrer"&gt;Sanity schema&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The app also includes a live content inspector. After the move, it shows &lt;code&gt;session-1-1&lt;/code&gt; at Table C with &lt;code&gt;1/4&lt;/code&gt; places booked:&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%2Ffuulc3s951z8ekzoc881.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%2Ffuulc3s951z8ekzoc881.jpg" alt="Sanity App SDK inspector showing session-1-1 at Table C with one of four places booked" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;App SDK reads this public schedule and its booking counts from Content Lake. Participant names, uploaded photos and organiser access data stay behind authorised server routes. The Sanity write token stays on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  A change has a review and a decision
&lt;/h3&gt;

&lt;p&gt;An edited plan becomes a saved proposal. Sanity Workflows tracks it through Reading, Review and either Applied or Discarded, so the organiser can return to a review and see what happened.&lt;/p&gt;

&lt;p&gt;There is a timing problem here: someone can join while the organiser is reviewing a move. A proposal that fitted the earlier bookings may no longer fit.&lt;/p&gt;

&lt;p&gt;Before applying it, the server checks the event and proposal revisions and recomputes the constraints. If the event has changed, the organiser has to recheck. Once approved, the plan, linked records and public schedule are saved together in one Content Lake transaction.&lt;/p&gt;

&lt;p&gt;App SDK subscribes to the public schedule's version. A change prompts the organiser workspace to refresh its private data through an authorised server route. Workflows records the review's progress; the server checks permission and validates the move.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/himanshu748/inkshift" rel="noopener noreferrer"&gt;Source code and setup instructions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;INKSHIFT uses Next.js and React, Sanity Content Lake, App SDK and Workflows. Photo reading uses Qwen3-VL through Hugging Face Inference Providers. The &lt;a href="https://github.com/himanshu748/inkshift/blob/main/docs/VERIFICATION.md" rel="noopener noreferrer"&gt;verification record&lt;/a&gt; covers booking preservation, concurrent changes, access recovery and private-data checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;I built INKSHIFT with two AI-native tools: Codex for the first build and the finish, and Claude Code for an upgrade pass in between. Every step ran against the real Sanity project, so each claim below comes from a test run or a live check.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pitch, then very short prompts
&lt;/h3&gt;

&lt;p&gt;The idea started as a note I pasted in: "A handwritten plan becomes a working, multiplayer app. Then you change the paper, and the app understands what changed without losing what people already did."&lt;/p&gt;

&lt;p&gt;My prompts after that were short. "go on it's for dev.to challenge", then "anything works also if that does not fit the hackathon let me know and switch over to another project". Codex read the Path Two rules before writing code and answered that it fit, because Sanity would hold the model the whole product depends on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first correction: a table is not a session
&lt;/h3&gt;

&lt;p&gt;The first model tied each game to its table. Moving Ticket to Ride from Table B to Table C would have replaced the session and dropped its bookings, which is the exact failure the product exists to prevent. Spaces, sessions and registrations became separate records with stable IDs, and a registration points to its session. Most of the later work follows from that decision.&lt;/p&gt;

&lt;p&gt;The same day we set a second rule: a region missing from a photo is uncertainty, not evidence of a deletion, and it goes to review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the models got stuck
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The vision provider rejected the schema.&lt;/strong&gt; Qwen3-VL through the Hugging Face router refused the bounding-box format. Codex fixed it by expressing each box as a fixed-length array of numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reader removed a booked game.&lt;/strong&gt; On the second photo of an edited plan, the reader decided Ticket to Ride was gone. Review blocked approval until I matched it back to the original session. After approval, the booking appeared at Table C. That run is why review is mandatory, not optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The schema deploy was refused.&lt;/strong&gt; The token Sanity provisioned for the project could write documents but could not deploy a schema. The app doesn't need it at runtime, so it waited until I deployed it with my own Sanity login four days later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App SDK warned during server rendering in production.&lt;/strong&gt; Moving the subscription provider behind a browser-only import fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel picked the wrong framework preset.&lt;/strong&gt; Committing an explicit Next.js configuration fixed the first deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I threw away
&lt;/h3&gt;

&lt;p&gt;I asked for a Three.js scroll world on the landing page. It worked: the paper became tables, pawns took their seats and the game moved from B to C. It also looked like a toy, with small pieces and handwriting you couldn't read. I told Claude the 3D looked bad and chose a replacement built from the real interface. The walkthrough on the homepage is now the actual screens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reaching into Workflows
&lt;/h3&gt;

&lt;p&gt;Workflows is in early access, so the agent worked from the docs, not from memory. Claude wrote the &lt;code&gt;inkshift-plan-change&lt;/code&gt; definition (Reading, then Review, then Applied or Discarded) and its adapter, then hit its session limit before wiring the routes and interface. Codex picked up that working copy, connected proposal creation, readings, corrections, approval and discard to the engine, and deployed definition v1.&lt;/p&gt;

&lt;p&gt;One detail from the docs shaped the design: the engine's checks are advisory, and only the Content Lake enforces anything. So the server still rechecks revisions, seats and the time slot before it writes, and a registration that arrives during review invalidates the stale proposal. Codex also added recovery for a plan decision whose workflow follow-up fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  The time machine
&lt;/h3&gt;

&lt;p&gt;After the first submission I wanted the strange part to be visible: ink on paper edits a live database, and the same people travel with their bookings as the paper changes. I asked Claude Code for a "paper time machine" and gave it one rule: do not fake history.&lt;/p&gt;

&lt;p&gt;Before any UI, it checked the rule against the real dataset with a GROQ read. 17 events had applied proposals, and in all 17 the latest applied preview matched the live plan exactly. Two problems came out of that read. 8 of 21 applied proposals were saved before &lt;code&gt;appliedVersion&lt;/code&gt; existed, so ordering falls back to the applied time. And nothing stored the plan before the first edit, so version 1 of a sample could only be rebuilt from code. That is why &lt;code&gt;planBefore&lt;/code&gt; now exists and why old gatherings say &lt;strong&gt;Reconstructed&lt;/strong&gt; on version 1.&lt;/p&gt;

&lt;p&gt;What went wrong: the first test run was flaky. A booking and an approval landed in the same millisecond, and the version-1 frame dropped the guest. The tests now run on a fixed clock, and a booking made at the exact moment of an apply counts as before it. The agent also hit its session limit while writing the component and picked the work up from the last commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing this post
&lt;/h3&gt;

&lt;p&gt;I pushed back on two drafts: "you've to establish it as completed product not incomplete" and "why'd you talk about how we made it instead of what product and how it uses sanity". That's why the post opens with the product. The build story lives here.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is still unverified
&lt;/h3&gt;

&lt;p&gt;Real handwriting (every image test used rendered typed sheets) and camera access on physical phones. The domain tests cover relocation, full destinations, identity ambiguity, cropped photos, time conflicts and capacity cuts. With 6 tests for the time machine (ordering, discarded readings left out, bookings followed by session ID, missing photos, reconstructed originals) there are 37 in all. A live race for the last place produced exactly one winner, and five booking IDs survived a relocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;p&gt;Project ID: &lt;code&gt;a5xdqsb7&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Dataset: &lt;code&gt;production&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Workflow: &lt;code&gt;inkshift-plan-change&lt;/code&gt;, version 1&lt;/p&gt;

&lt;p&gt;To use INKSHIFT for your own gathering, &lt;a href="https://inkshift.vercel.app" rel="noopener noreferrer"&gt;create an event&lt;/a&gt;, add its plan and share the participant invite. You can return through &lt;strong&gt;Your gatherings&lt;/strong&gt;, or restore organiser access on another device with your private backup code.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>FinePrint: an agent that checks your hackathon entry against the rules it reads</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:01:07 +0000</pubDate>
      <link>https://dev.to/himanshu_748/fineprint-an-agent-that-checks-your-hackathon-entry-against-the-rules-it-reads-5fpa</link>
      <guid>https://dev.to/himanshu_748/fineprint-an-agent-that-checks-your-hackathon-entry-against-the-rules-it-reads-5fpa</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Hackathon rules live in three places: the challenge page, the FAQ and the official contest rules. They don't always agree, and one missed line can disqualify a good project.&lt;/p&gt;

&lt;p&gt;FinePrint is an agent you can ask. "I started my app in August. I added Sanity this week. Can I enter Path One?" It reads a Sanity Knowledge Base built from the official rules, proposes facts with exact quotes from your question and calls a typed checker over structured requirement records. The answer shows the rules it read, the facts it accepted and every tool call it made.&lt;/p&gt;

&lt;p&gt;A keyword search finds the sentence about the entry period. It can't tell you that "started in August" blocks the development-start rule while reusing components is still allowed, or that the contest rules changed their entry limit halfway through the challenge. That needs structure: each requirement is a record with its conditions, its event and a reference to the source version it came from.&lt;/p&gt;

&lt;p&gt;I picked this challenge as the first rule pack, and it changed under me. On September 20 the FAQ said "only one submission per path is allowed" while the contest rules said "There is no limit on the number of Entries you may submit during the Entry Period." FinePrint marked two entries in one path &lt;strong&gt;Rules unclear&lt;/strong&gt; and gave a question for the organizer. By September 24 DEV had replaced that contest-rules line with "Only one submission per path is allowed. This is to encourage quality over quantity." I published a new rule pack, &lt;code&gt;2026-09-24.1&lt;/code&gt;, and a saved two-entry review now shows the check going from Rules unclear to &lt;strong&gt;Blocked&lt;/strong&gt;, with both dated quotes. The old pack stays in Sanity, so the change is visible, not overwritten.&lt;/p&gt;

&lt;p&gt;I added GIBC V2’s Open Invention track as a second curated event. FinePrint checks 19 Sanity requirements and 18 GIBC requirements. &lt;strong&gt;Compare events&lt;/strong&gt; carries team size and development date across both, while leaving event-specific answers unknown. An answer about what existed before September 18 cannot establish what existed before July 11. Each check can be Supported, Blocked, Missing fact, Rules unclear or Not applicable. Changing a fact highlights the affected findings. Personal reviews autosave in the browser and can be downloaded as Markdown. A facts-only backup moves work between browsers without importing an unverified verdict.&lt;/p&gt;

&lt;p&gt;Two curated events don't cover the hackathon you're entering next, so FinePrint can also read any public rules page. Paste its link and the model proposes requirements. Every requirement must quote the page word for word, or it is dropped and listed. FinePrint maps each quote onto its fixed fact vocabulary (team size, age, build window, entries and so on) and builds the typed condition itself. Anything it can't map, or any number the quote doesn't contain, becomes a &lt;strong&gt;Check yourself&lt;/strong&gt; rule that can never show as Supported. Imported events are labeled "Imported from zero-origin.devpost.com, not reviewed" (with the real host) and stay in your browser, never in the Sanity dataset.&lt;/p&gt;

&lt;p&gt;Saved reports retain the requirements and sources used at check time. When a new curated pack is published, FinePrint identifies added, removed or changed requirements and follows source references to the affected findings. A capture-date refresh alone does not pretend that a condition changed. This checks curated Sanity records; it does not monitor organizer websites.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://fineprint-kappa.vercel.app" rel="noopener noreferrer"&gt;Try FinePrint&lt;/a&gt; · &lt;a href="https://fineprint-kappa.vercel.app/review" rel="noopener noreferrer"&gt;Open the review desk&lt;/a&gt; · &lt;a href="https://github.com/himanshu748/fineprint" rel="noopener noreferrer"&gt;Source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 65-second run on the deployed app, recorded September 25 with nothing sped up: the dated rule change, the agent answering a two-entries question through Sanity Context and an imported Devpost rules page.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Vy0qynByqZ4" width="710" height="399"&gt;
  &lt;/iframe&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%2F4z2hqb227rwx3oyzrrmy.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%2F4z2hqb227rwx3oyzrrmy.png" alt="FinePrint compares an illustrative five-person project against the Sanity and GIBC rule packs. Team size and the August start date are blocked for Sanity and supported for GIBC." width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Browser capture from September 22. The example also declares that every member is a student. Both reviews retain unanswered questions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Create a Sanity review, enter a team size of five and a development date of August 23, 2026, then open &lt;strong&gt;Compare events&lt;/strong&gt;. Both checks are blocked for Sanity and supported for GIBC. GIBC still needs answers to its other requirements. Create a separate GIBC review to continue.&lt;/p&gt;

&lt;p&gt;Open &lt;strong&gt;Try a rule-change rehearsal&lt;/strong&gt; to lower a hypothetical team limit from six to three. One of the eighteen checks is affected. The rehearsal is labeled and local; it never changes an official source or saved review.&lt;/p&gt;

&lt;p&gt;To try the agent, open any Sanity review. &lt;strong&gt;Ask FinePrint&lt;/strong&gt; sits at the top of the desk. Choose &lt;strong&gt;Two entries, one path&lt;/strong&gt; and press &lt;strong&gt;Ask FinePrint&lt;/strong&gt;; the answer now cites the September 24 rule and the typed check is Blocked. Inspect the actual trace and the source interpretation beside the typed result. &lt;strong&gt;Add these facts to my review&lt;/strong&gt; preserves the other answers.&lt;/p&gt;

&lt;p&gt;To check another event, start a review and pick &lt;strong&gt;Another hackathon (paste its rules link)&lt;/strong&gt;. Try &lt;code&gt;https://zero-origin.devpost.com/rules&lt;/code&gt;, then press &lt;strong&gt;Read the rules&lt;/strong&gt;. You get a review of that event's requirements, with the ones FinePrint could not map listed as Check yourself. &lt;strong&gt;Ask FinePrint&lt;/strong&gt; works on it too.&lt;/p&gt;

&lt;p&gt;The homepage replays a dated, recorded source explanation. Another example lets you change whether an entire application or only its components existed before the event. The origin check changes, while the August development date remains blocked.&lt;/p&gt;

&lt;p&gt;The landing page includes the four recorded comparison runs. Switch between team size and development date, then expand a source read to see which Sanity Knowledge Base entry and structured records were used. The GIBC date example keeps the model disagreement visible.&lt;/p&gt;

&lt;p&gt;Live requests share a limit of five runs per ten minutes in a Vercel regional bucket. The question and any form facts explicitly included with it go to the model provider. They are not written to the public Sanity dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;The public &lt;code&gt;production&lt;/code&gt; dataset in project &lt;code&gt;cxbqxkq6&lt;/code&gt; contains two competitions whose current packs hold 37 requirements, plus the dated source versions each pack quotes. Superseded packs stay in the dataset. Each requirement references its event’s official sources. Event IDs, pack versions and explicit source references keep identical concepts such as team size separate. A GROQ query loads the pack, and Zod validates it before the checker uses it.&lt;/p&gt;

&lt;p&gt;These relationships also make the change review possible. A saved finding records the requirement and source versions it used. If a curator changes the team-size requirement, FinePrint follows those references to the team finding. If only the capture date changes, it leaves the condition unchanged. Sanity holds the content and its relationships; the app uses them for retrieval, comparisons and report updates.&lt;/p&gt;

&lt;p&gt;Knowledge Base &lt;code&gt;kbyrY7h8fTnL&lt;/code&gt; reads the public dataset through the dedicated &lt;code&gt;fineprint&lt;/code&gt; MCP endpoint. Its purpose now covers Sanity and GIBC Open Invention, with separate event paths, exact rule titles and source versions. The app uses a server-side Context Viewer token. The browser never receives that credential.&lt;/p&gt;

&lt;p&gt;For a question, the agent follows this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call &lt;code&gt;initial_context&lt;/code&gt; to get the Knowledge Base outline.&lt;/li&gt;
&lt;li&gt;Select relevant paths and call &lt;code&gt;knowledge_base_read&lt;/code&gt;. The server validates each path against the outline before reading it.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;check_requirements&lt;/code&gt; with quoted project facts and an independent interpretation of the relevant rules.&lt;/li&gt;
&lt;li&gt;Validate those proposals and run the typed conditions. Return the model’s interpretation alongside the checker’s result, including disagreements.&lt;/li&gt;
&lt;li&gt;Write the answer and cite retrieved entry paths. The question flow removes and discloses citation paths that were never read, and rejects an answer if no valid citations remain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The question flow allows four model rounds, six entry reads and 45,000 source characters. The trace shows the actual calls and elapsed times. A provider error produces an error state with the completed steps.&lt;/p&gt;

&lt;p&gt;The Knowledge Base reads the curated dataset records plus three official pages as website sources: the challenge page, the contest rules and DEV's general hackathon rules. When those sources were added, Context raised two conflicts. One was mine: an entry said development had to start after the opening moment, while the rules say "during, and not prior to, the Entry Period" and the checker accepts the opening moment. I resolved it in favor of the source. The other was the entry limit. The contest rules page Context crawled that day already said one submission per path, while my September 20 dataset record still quoted "no limit". Context flagged the stale record before I had noticed the change myself. I resolved it in favor of the live page, added a standing instruction that dates the old wording as a rule change and published the new pack.&lt;/p&gt;

&lt;p&gt;For an imported event, the agent still reads the Knowledge Base for how FinePrint weighs sources, and uses a separate &lt;code&gt;imported_rules_read&lt;/code&gt; tool over the quoted requirements. The trace names both, and citations are limited to what was actually read in that run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the live runs showed
&lt;/h2&gt;

&lt;p&gt;The two-event test made four real calls to Sanity Context and Modal: team size and development date, once for each event. All four typed checks matched my authored labels. The model agreed on three. Every answer cited a relevant retrieved entry, and the calls took 9.2 to 14.9 seconds.&lt;/p&gt;

&lt;p&gt;On the fourth question, the model asked for a timezone even though August 23 was well inside GIBC’s July-to-October build window. It called the result unclear; the typed checker supported the supplied date. FinePrint kept the disagreement visible. That is a failure I want to inspect before relying on the explanation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/himanshu748/fineprint/blob/main/evaluation/multi-event-context.json" rel="noopener noreferrer"&gt;recorded questions, answers and tool traces&lt;/a&gt; include that disagreement. Four questions are a small integration check, not an accuracy study.&lt;/p&gt;

&lt;p&gt;In a September 22 local run against the real Sanity and Modal services, the August question completed in 12.8 seconds. The agent read four entries and accepted three quoted facts. The date remained a month, with the inferred challenge year disclosed. It refused to treat “started in August” as proof that the whole application already existed. The model still interpreted the prior-work rule as blocked, while the typed check needed that missing fact. Both results were visible.&lt;/p&gt;

&lt;p&gt;A second run took 11.4 seconds and read three entries. For “I am planning two entries in Path One,” both the source interpretation and the typed checker kept the submission limit unclear. That was correct against the September 20 rules; against the September 24 pack the same question is Blocked.&lt;/p&gt;

&lt;p&gt;After deployment, the same question ran in a browser without an access code in 6.4 seconds. Its trace showed eight actual steps, including three Knowledge Base reads and the requirement check. A separate hosted check verified that the per-finding explanation still worked.&lt;/p&gt;

&lt;p&gt;I tested the importer on three real pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Rules found&lt;/th&gt;
&lt;th&gt;Mapped to a check&lt;/th&gt;
&lt;th&gt;Check yourself&lt;/th&gt;
&lt;th&gt;Quotes dropped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zero Origin (Devpost rules)&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;This challenge's DEV page&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A lablab.ai event plus its guidelines page&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first runs mapped too loosely. "AI tools are permitted" became "AI use disclosed", and a start date quoted without a timezone came out nine hours early. I added two deterministic guards: a date quote must name a timezone, and a quote must contain words for the requirement it's mapped to. Those guards are why most rules end up as Check yourself. On production, the Zero Origin import took 4.3 seconds. In a local run against the real Sanity and Modal services, a question on that imported review ("We are a team of five and the youngest of us is 16") took three model rounds and 18.5 seconds, and team size came out Blocked from both the agent and the checker.&lt;/p&gt;

&lt;p&gt;The importer's limits are real. Pages that render their rules with JavaScript expose little text, so a lablab event page gives about 1,000 characters and you need to add its rules page. The model's mappings aren't reviewed and vary between runs; the labels and quote checks keep that visible.&lt;/p&gt;

&lt;p&gt;Those runs test particular questions and pages. They do not establish general eligibility accuracy.&lt;/p&gt;

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

&lt;p&gt;FinePrint uses Next.js, TypeScript, Zod, Sanity Content Lake and Context MCP. DeepSeek V4.1 Flash runs on an existing Modal endpoint. Vercel hosts the app.&lt;/p&gt;

&lt;p&gt;Start with &lt;a href="https://github.com/himanshu748/fineprint/blob/main/src/lib/question-agent.ts" rel="noopener noreferrer"&gt;&lt;code&gt;question-agent.ts&lt;/code&gt;&lt;/a&gt; for the tool loop, &lt;a href="https://github.com/himanshu748/fineprint/blob/main/src/lib/question-facts.ts" rel="noopener noreferrer"&gt;&lt;code&gt;question-facts.ts&lt;/code&gt;&lt;/a&gt; for quote validation and &lt;a href="https://github.com/himanshu748/fineprint/blob/main/src/lib/engine.ts" rel="noopener noreferrer"&gt;&lt;code&gt;engine.ts&lt;/code&gt;&lt;/a&gt; for the checks.&lt;/p&gt;

&lt;p&gt;The automated suite includes migration of older reviews, event isolation, unknown facts, changed source dependencies, added and removed rules, deadline changes and bounded API requests. Production smoke checks also exercise anonymous comparisons with cloud providers disabled. The engine matches 42 authored regression scenarios. An earlier recorded model baseline, given the nine Knowledge Base entries that existed at that time, matches 39 of those labels. I wrote the fixtures and the rule pack; that comparison measures agreement with my labels, not independent accuracy or retrieval quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with coding agents
&lt;/h2&gt;

&lt;p&gt;I started with a broader pitch for a pre-submission checking agent, then chose “Revise the concept together before building.” My next instruction was “uncertain eligibility.” That narrowed the project to one event and a concrete applicability problem.&lt;/p&gt;

&lt;p&gt;Codex built the first version. I chose Modal when it asked for a model provider, approved a public rules dataset and connected a read-only Context token. I initially asked for a Three.js landing page. After reviewing it, I chose Claude’s product-focused replacement without 3D, along with a public question flow and visible tool calls. Claude stopped at its session limit partway through that upgrade. Codex recovered the unfinished work and continued it.&lt;/p&gt;

&lt;p&gt;I then asked for a product people could keep using and chose multi-event comparison plus rule-change impact. That introduced another boundary: a project fact can travel between events, but a declaration about an event’s requirements usually cannot.&lt;/p&gt;

&lt;p&gt;The last addition was mine to push for: "fineprint should be able to work towards all of the hacks if possible". Claude built the importer, and the live runs above are where its first mappings fell short.&lt;/p&gt;

&lt;p&gt;The hardest boundary is deciding what a person stated. An English question does not establish an English submission; planning an integration does not establish a working integration. FinePrint rejects several such shortcuts, keeps unknown facts visible and lets the person inspect the quoted facts before applying them to the desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;code&gt;cxbqxkq6&lt;/code&gt;, dataset &lt;code&gt;production&lt;/code&gt; (public, read-only rule records)&lt;/li&gt;
&lt;li&gt;Public dataset query: &lt;a href="https://cxbqxkq6.api.sanity.io/v2025-02-19/data/query/production?query=*%5B_type%3D%3D%22competition%22%5D%7Btitle%2C%22requirements%22%3Acount(requirements)%7D" rel="noopener noreferrer"&gt;competitions with their requirement counts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Document types: &lt;code&gt;competition&lt;/code&gt;, &lt;code&gt;requirement&lt;/code&gt;, &lt;code&gt;sourceVersion&lt;/code&gt;, &lt;code&gt;reviewRubric&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Knowledge Base: &lt;code&gt;kbyrY7h8fTnL&lt;/code&gt;, served through the named Context MCP endpoint &lt;code&gt;fineprint&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No login is needed to try FinePrint. Rule checks run without the model; live agent answers share a limit of five runs per ten minutes.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Pass It On: donate an explanation someone else can use</title>
      <dc:creator>Himanshu Kumar</dc:creator>
      <pubDate>Sat, 05 Sep 2026 18:00:09 +0000</pubDate>
      <link>https://dev.to/himanshu_748/pass-it-on-give-someone-an-answer-they-can-pass-on-lcf</link>
      <guid>https://dev.to/himanshu_748/pass-it-on-give-someone-an-answer-they-can-pass-on-lcf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Pass It On lets you donate an explanation. Speak or type what a handbook means, correct it against the source, and leave a reviewed answer that someone else can read, hear, or save.&lt;/p&gt;

&lt;p&gt;I built this around giving knowledge and time. A useful explanation should remain available when the next person asks the same question. The source needs to travel with it, too.&lt;/p&gt;

&lt;p&gt;The first collection helps someone make their first open-source contribution. It uses selected excerpts from GitHub’s &lt;a href="https://opensource.guide/how-to-contribute/" rel="noopener noreferrer"&gt;How to Contribute to Open Source&lt;/a&gt;, with attribution, a pinned revision, and the CC BY 4.0 license. It covers three questions: whether you need to write code, what to check before starting, and how to prepare a pull request.&lt;/p&gt;

&lt;p&gt;You can read a prepared answer in English or Hindi as soon as you arrive, open its supporting excerpt, hear it through ElevenLabs, and save a copy with the source attached. Prepared answers are labelled as Pass It On adaptations. They do not count as community reviews. A volunteer can then contribute an explanation, correct it with Gemini, and leave a source-linked answer for the next visitor.&lt;/p&gt;

&lt;p&gt;A student coding club could use this collection during an open-source introduction: a beginner reads or hears an answer, checks the source, and passes the link to a classmate. That is the use case I want to test with a group. I have verified the hosted workflow, but I have not measured adoption or observed an independent participant completing it.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://pass-it-on-himanshu.vercel.app/app?collection=open-source&amp;amp;question=oss-non-code&amp;amp;language=hi" rel="noopener noreferrer"&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%2Fxer9jzn9egy80m93abtr.png" alt="Pass It On workspace showing the real open-source guide and its attributed answer." width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pass-it-on-himanshu.vercel.app/app?collection=open-source&amp;amp;question=oss-non-code&amp;amp;language=hi" rel="noopener noreferrer"&gt;Read the shared Hindi answer&lt;/a&gt; · &lt;a href="https://pass-it-on-himanshu.vercel.app/app?collection=open-source&amp;amp;question=oss-non-code&amp;amp;language=en" rel="noopener noreferrer"&gt;Read the English starter answer&lt;/a&gt; · &lt;a href="https://pass-it-on-himanshu.vercel.app/" rel="noopener noreferrer"&gt;Open the website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start with the answer. Open a supporting excerpt, then select &lt;strong&gt;Listen to this answer&lt;/strong&gt;. &lt;strong&gt;Copy question link&lt;/strong&gt; sends the next reader to the same question and language. &lt;strong&gt;Save answer&lt;/strong&gt; downloads the explanation, exact quotes, review or preparation date, attribution, license, source version, and return link for offline reading. The saved copy says that it does not update.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://pass-it-on-himanshu.vercel.app/app?collection=open-source&amp;amp;question=oss-non-code&amp;amp;language=en&amp;amp;example=correction" rel="noopener noreferrer"&gt;try contributing&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the deliberately incorrect example. It says only programmers can contribute and documentation does not count. Gemini should identify the contradiction and ask for a correction.&lt;/li&gt;
&lt;li&gt;Use the corrected example and check again. Open &lt;strong&gt;Review&lt;/strong&gt;, compare the claims with the quoted source, and complete both acknowledgements.&lt;/li&gt;
&lt;li&gt;Return to &lt;strong&gt;Questions&lt;/strong&gt;. Pass its link to a separate browser session and retrieve the shared answer there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also start a spoken interview: hear the question, consent to recording, speak your response, and edit the transcript before continuing. ElevenLabs handles transcription and speech; Gemini checks the explanation and writes the follow-up.&lt;/p&gt;

&lt;p&gt;The sponsor step works on Solana devnet. The earlier fictional-grant practice flow supplied the payment evidence: &lt;a href="https://explorer.solana.com/tx/5dufRyjkreVxLzKd9dqJpSuSmiHooLVyq2MmWUXHSN9PH9zckCGCWD5AkoDsPXFX7AG3BFnGu6pkQDeVP4pgcftX?cluster=devnet" rel="noopener noreferrer"&gt;This finalized transfer paid 0.001 test SOL&lt;/a&gt; to the demo contributor. Its memo identifies the approved contribution. Test SOL has no monetary value. People can contribute and reuse answers whether or not a sponsor pays the bounty.&lt;/p&gt;

&lt;p&gt;The fictional scholarship handbook remains in a separate &lt;strong&gt;Practice&lt;/strong&gt; collection for rehearsing corrections. Its answers and payment evidence are kept separate from the real guide. Visitors can try a clearly labelled demo reviewer role in either collection; it is not independently authenticated review. The real guide is general advice, so the target project’s own contribution instructions still matter.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/himanshu748/pass-it-on" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; · &lt;a href="https://github.com/himanshu748/pass-it-on/blob/main/docs/reuse-verification.md" rel="noopener noreferrer"&gt;Verification and known limits&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project began as Openhand, a giving ledger, and became Pass It On during this challenge. The earlier ledger remains at &lt;code&gt;/ledger&lt;/code&gt;. I used Express, browser JavaScript, and Vercel. Claude and Codex helped with implementation, design, testing, and writing.&lt;/p&gt;

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

&lt;p&gt;Each technology handles a specific part of the exchange:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;What it does for the person using the app&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs&lt;/td&gt;
&lt;td&gt;Lets a volunteer speak an explanation and a reader hear an answer. It also voices the correction question, so the interview can continue aloud.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI / Gemini&lt;/td&gt;
&lt;td&gt;Compares the explanation with the handbook, flags contradictions, and asks for corrections. It returns claims paired with exact source excerpts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Stores approved answers for other sessions and joins question requests with reviewed answers by language and source version. Volunteers can see which questions still need an explanation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;Links a sponsor's devnet payment to a particular contribution through the transaction memo. The server verifies finalization, sender, recipient, and amount before marking it paid.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I kept the source check and human review separate. The server rejects quotes that do not occur in the handbook. That catches invented excerpts, but an exact quote can still be interpreted badly. A successful AI check therefore opens review; it cannot approve an answer or authorize a payment by itself.&lt;/p&gt;

&lt;p&gt;The next reader's experience drove the latest changes. First visits open on available answers. A direct link selects the question and language. Saved handouts carry their source and limitations. Shared links contain no session, review, or payment credentials. Hindi answer text and English source excerpts have separate language labels for assistive technology.&lt;/p&gt;

&lt;p&gt;All 40 tests pass. The test suite covers collection isolation, prepared-answer attribution, proof tampering and expiry, review gates, provider failures, exact payment matching, and the sharing/export boundaries. On the hosted real-guide collection, Gemini rejected the incorrect example, accepted the correction, and returned Hindi claims with source excerpts. The demo review saved the answer to Snowflake; a separate request retrieved it without the contributor’s session. ElevenLabs played both the prepared Hindi answer and the newly reviewed answer. The live check exposed the voice key’s credit cap; after raising it within the existing free allowance, the reviewed Hindi answer played successfully. These checks were agent-operated, not independent user research. The project owner confirmed microphone recording; the earlier automated transcription check used generated sample audio.&lt;/p&gt;

&lt;p&gt;Before opening community review to a real group, I need authenticated reviewers, correction and removal tools, and durable abuse controls. Adding a group’s own handbook is also still future work. The current bounty uses a fixed devnet recipient and a direct transfer; it is not escrow and does not guarantee exactly-once payment across simultaneous sponsors. The next useful test is a coding club trying this real guide, with a second person finding an answer without the original volunteer present.&lt;/p&gt;

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

&lt;p&gt;Best Use of Google AI, Best Use of ElevenLabs, Best Use of Snowflake, and Best Use of Solana.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <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>
  </channel>
</rss>
