<?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: S M Tahosin</title>
    <description>The latest articles on DEV Community by S M Tahosin (@tahosin).</description>
    <link>https://dev.to/tahosin</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%2F3886453%2F0f012a95-ad46-4c17-97e8-125ec8b4978d.png</url>
      <title>DEV Community: S M Tahosin</title>
      <link>https://dev.to/tahosin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tahosin"/>
    <language>en</language>
    <item>
      <title>Why Should a Mother with ALS Sound Like a Microwave? Building Gift of Voice with Gemini and ElevenLabs</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:26:24 +0000</pubDate>
      <link>https://dev.to/tahosin/why-should-a-mother-with-als-sound-like-a-microwave-building-gift-of-voice-with-gemini-and-4pa9</link>
      <guid>https://dev.to/tahosin/why-should-a-mother-with-als-sound-like-a-microwave-building-gift-of-voice-with-gemini-and-4pa9</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;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The greatest act of generosity is not giving what you have in abundance, but giving someone the ability to express what is locked inside their heart."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Over &lt;strong&gt;65 million people worldwide&lt;/strong&gt; live with profound speech loss caused by conditions like ALS (Amyotrophic Lateral Sclerosis), post-stroke aphasia, cerebral palsy, and vocal trauma. &lt;/p&gt;

&lt;p&gt;For decades, the standard assistive tools (Augmentative and Alternative Communication, or AAC) have suffered from two heartbreaking problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Financial Barrier:&lt;/strong&gt; Proprietary hardware costs anywhere from &lt;strong&gt;$5,000 to $12,000&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotional Robbery:&lt;/strong&gt; The voices sound like flat, monotone 1990s robots. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Think about what that means in human terms: When a mother with ALS wants to say &lt;em&gt;"I love you"&lt;/em&gt; to her child, or a patient in an ICU ward wants to thank a night nurse for a warm blanket, a mechanical monotone voice strips away every trace of tenderness, laughter, and vulnerability. The patient's mind is vibrant, their heart is full of love, but their output sounds like a microwave beep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GiftOfVoice&lt;/strong&gt; was born out of a simple conviction: &lt;strong&gt;Generosity is more than donating money.&lt;/strong&gt; The most profound gift one human being can give another is a voice when theirs has been taken away.&lt;/p&gt;

&lt;p&gt;GiftOfVoice is an open-source, humanitarian web platform that combines &lt;strong&gt;Google Gemini 3.6 Flash&lt;/strong&gt; and &lt;strong&gt;ElevenLabs&lt;/strong&gt; into an empathic assistive communication engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Empathy Soundboard (Accessible AAC Grid):&lt;/strong&gt; Large, high-contrast, keyboard-navigable tiles organized by semantic human needs (&lt;em&gt;Gratitude &amp;amp; Care, Daily Comfort, Family &amp;amp; Love, Humor &amp;amp; Social, Urgent &amp;amp; Medical&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual Intent Expander (Google Gemini):&lt;/strong&gt; Recognizes that motor fatigue makes typing arduous. A patient taps just two or three shorthand concepts (e.g., &lt;code&gt;[Water] + [Pillow] + [Nurse]&lt;/code&gt;), and Gemini contextually expands them into natural, dignified, first-person sentences adapted to the chosen emotional tone (&lt;em&gt;Grateful, Loving, Playful, Peaceful, Urgent&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Voice Generosity Bank (ElevenLabs):&lt;/strong&gt; Delivers authentic human warmth, breath, and emotional prosody using vocal timbres donated by healthy community volunteers, replacing robotic monotone with living human presence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Voice Generosity Studio:&lt;/strong&gt; An ethical framework where community members can record and donate their vocal cadence to the open repository for speech-impaired individuals.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;h3&gt;
  
  
  🚀 Live Interactive Application
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live URL:&lt;/strong&gt; &lt;a href="https://x-tahosin.github.io/giftofvoice/" rel="noopener noreferrer"&gt;https://x-tahosin.github.io/giftofvoice/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/x-tahosin/giftofvoice" rel="noopener noreferrer"&gt;https://github.com/x-tahosin/giftofvoice&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚡ 1-Click Judge Showcase Scenarios
&lt;/h3&gt;

&lt;p&gt;To ensure judges on any device or connection speed experience instant results, GiftOfVoice features &lt;strong&gt;4 zero-latency pre-rendered clinical &amp;amp; family scenarios&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ICU Night Ward: Thanking the Overworked Nurse&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Concepts:&lt;/em&gt; &lt;code&gt;[Thank you so much]&lt;/code&gt; + &lt;code&gt;[Warm blanket]&lt;/code&gt; + &lt;code&gt;[Your kindness]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Gemini Output:&lt;/em&gt; &lt;em&gt;"Thank you so much, Sarah. You have been working so hard all night, and bringing me this warm blanket gave me so much peace and comfort. I truly appreciate your kindness."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Voice:&lt;/em&gt; Grandma Margaret (Warm, gentle, mature grandmotherly tone)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedtime Blessing: Grandchild's 7th Birthday&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Concepts:&lt;/em&gt; &lt;code&gt;[I love you deeply]&lt;/code&gt; + &lt;code&gt;[So proud of you]&lt;/code&gt; + &lt;code&gt;[Hold my hand]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Gemini Output:&lt;/em&gt; &lt;em&gt;"Happy birthday, my sweet Leo! I love you with all of my heart, and I am so deeply proud of the brave, curious boy you are growing into today. Come hold my hand!"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Voice:&lt;/em&gt; Grandpa Bill (Elderly grandfatherly warmth, raspy wisdom)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family Dinner: Breaking the Silence with a Dad Joke&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Concepts:&lt;/em&gt; &lt;code&gt;[Tell me a funny joke]&lt;/code&gt; + &lt;code&gt;[Do not worry about me]&lt;/code&gt; + &lt;code&gt;[You look amazing]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Gemini Output:&lt;/em&gt; &lt;em&gt;"Hey everyone, please don’t worry about me over here! I might be letting the technology do the talking today, but I definitely still have the best sense of humor in this room."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Voice:&lt;/em&gt; Roger (Laid-back, friendly timbre)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALS Caregiver Check-in: Calm Posture &amp;amp; Hydration Adjustment&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Concepts:&lt;/em&gt; &lt;code&gt;[Cold water please]&lt;/code&gt; + &lt;code&gt;[Adjust my pillow]&lt;/code&gt; + &lt;code&gt;[Please stay with me]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Gemini Output:&lt;/em&gt; &lt;em&gt;"Could you please help adjust my pillow slightly to the right, and let me take a small sip of cold water? Thank you so much for your patience, David."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Voice:&lt;/em&gt; Antoni (Deep, reassuring, steady presence)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;&lt;strong&gt;Repository Link:&lt;/strong&gt; &lt;a href="https://github.com/x-tahosin/giftofvoice" rel="noopener noreferrer"&gt;https://github.com/x-tahosin/giftofvoice&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire project is open-source under the MIT license, with full clean architecture, measurable WCAG 2.1 AAA accessibility compliance, and unified Express + Vite production serving.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  The 5-Step Pipeline Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------------------------+
|                                    GiftOfVoice Client                                   |
|                                                                                         |
|  [ 1. AAC Soundboard ]  ---&amp;gt;  [ 2. Context &amp;amp; Tone ]  ---&amp;gt;  [ 3. Gemini Expansion ]      |
|    - Sparse Concepts            - Grateful / Loving          - Natural, Concise,        |
|    - Touch Targets              - Recipient Context            Expressive Variations    |
+----------------------------------------------------------------------------+------------+
                                                                             |
                                                                             v
+----------------------------------------------------------------------------+------------+
|                                 Express Backend Proxy                                   |
|                                                                                         |
|  POST /api/expand-intent  ==========&amp;gt;  Google Generative Language API (gemini-3.6-flash)|
|  POST /api/synthesize-voice ========&amp;gt;  ElevenLabs TTS API (eleven_turbo_v2_5)           |
+----------------------------------------------------------------------------+------------+
                                                                             |
                                                                             v
+-----------------------------------------------------------------------------------------+
|                                  Audio Delivery Layer                                   |
|                                                                                         |
|  [ Pre-rendered Preset Audio ]  OR  [ Live Base64 Audio Stream ]  OR  [ Web Speech API ]|
|          (Zero Latency)                     (Real-Time)                   (Offline)     |
+-----------------------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Google Gemini as the Cognitive Intent Expander
&lt;/h3&gt;

&lt;p&gt;A critical mistake in assistive tech is assuming that an LLM should just be a chatbot. In AAC, patients experience severe motor fatigue. Typing full sentences with eye-tracking or single-switch clicks can take 5 to 10 exhausting minutes.&lt;/p&gt;

&lt;p&gt;We engineered a prompt for &lt;strong&gt;Gemini 3.6 Flash&lt;/strong&gt; that acts as an empathic co-pilot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Takes sparse semantic primitives (&lt;code&gt;[Water] + [Nurse]&lt;/code&gt;) and recipient context.&lt;/li&gt;
&lt;li&gt;Analyzes the desired emotional tone (&lt;em&gt;Grateful, Loving, Playful, Peaceful, Urgent&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;Outputs 3 distinct first-person variations:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natural (Balanced):&lt;/strong&gt; The recommended everyday conversational sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concise (Direct):&lt;/strong&gt; Minimal friction for urgent or clinical clarity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expressive (Heartfelt):&lt;/strong&gt; Emotionally rich and vulnerable for intimate family moments.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/services/gemini.js snippet&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are GiftOfVoice's Contextual Intent Expander, giving dignity and expressive speech to individuals with severe speech impairments.
Concepts: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"
Tone: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
Recipient: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
Task: Formulate 3 variations in first person ("I") with authentic human prosody and feeling.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. ElevenLabs as the Voice Generosity Synthesizer
&lt;/h3&gt;

&lt;p&gt;Monotone speech synthesis strips away humanity. To solve this, we integrated ElevenLabs using dynamic prosody mapping based on emotional tone:&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="c1"&gt;// server/services/elevenlabs.js snippet&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getVoiceSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loving&lt;/span&gt;&lt;span class="dl"&gt;'&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;stability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarity_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use_speaker_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarity_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use_speaker_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;peaceful&lt;/span&gt;&lt;span class="dl"&gt;'&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;stability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarity_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use_speaker_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urgent&lt;/span&gt;&lt;span class="dl"&gt;'&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;stability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarity_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use_speaker_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// grateful&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;stability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;similarity_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;use_speaker_boost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Lower stability and higher style exaggeration allow laughter, emotional warmth, and natural breath pauses to emerge in playful and loving tones, while higher stability ensures rock-solid clarity for medical check-ins.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Measurable Accessibility (WCAG 2.1 AAA)
&lt;/h3&gt;

&lt;p&gt;Accessibility was baked into every line of CSS and HTML:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Mouse Dependency:&lt;/strong&gt; Full keyboard navigation (&lt;code&gt;Tab&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Space&lt;/code&gt;) with 3px amber focus rings (&lt;code&gt;focus-visible&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-Contrast Mode:&lt;/strong&gt; Instant toggle between deep aurora theme and pure black/white/yellow high-contrast mode for visually impaired users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum 48px Touch Targets:&lt;/strong&gt; All soundboard buttons exceed 88px for motor-impaired users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Fail Fallback:&lt;/strong&gt; If internet access drops or cloud rate limits are reached, the application gracefully falls back to the client-side Web Speech API.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;GiftOfVoice was intentionally built to embody the spirit of the challenge across two key partner technologies:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Best Use of ElevenLabs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Replaces cold robotic TTS with living human timbre.&lt;/li&gt;
&lt;li&gt;Dynamic emotional prosody mapping (stability, style, and similarity tuned per emotional state).&lt;/li&gt;
&lt;li&gt;Community Voice Bank concept that grounds voice synthesis in humanitarian generosity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Best Use of Google AI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Powered by &lt;strong&gt;Gemini 3.6 Flash&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Moves beyond generic chatbot tropes by using Gemini as an intent-expansion intelligence that bridges human thought and physical disability.&lt;/li&gt;
&lt;li&gt;Provides multi-option contextual dialogue in sub-second response times.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ❤️ Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Generosity isn't just what we give from our wallets; it is what we restore to our fellow human beings. When we give a voice back to someone who has spent years in silence, we aren't just writing code; we are giving them back their seat at the family dinner table.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you to DEV, Google AI, and ElevenLabs for hosting this inspiring challenge!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>a11y</category>
    </item>
    <item>
      <title>The 4-Byte Ghost: How a Single Emoji Brought Down Our Payment Pipeline</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 23 Aug 2026 20:14:00 +0000</pubDate>
      <link>https://dev.to/tahosin/the-4-byte-ghost-how-a-single-emoji-brought-down-our-payment-pipeline-5o8</link>
      <guid>https://dev.to/tahosin/the-4-byte-ghost-how-a-single-emoji-brought-down-our-payment-pipeline-5o8</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;p&gt;We are a growing SaaS startup. Our background architecture isn't exactly Netflix scale, but it’s robust: a cluster of three modest Celery/RabbitMQ worker nodes handling thousands of daily webhooks, payment syncs, and email blasts. To guarantee no data was ever lost during worker crashes, we configured Celery with &lt;code&gt;acks_late=True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was a completely ordinary Tuesday afternoon when our entire background processing pipeline just... stopped. &lt;/p&gt;

&lt;p&gt;A queue that normally drains in milliseconds was suddenly backed up with 15,000 pending tasks. CPU usage across all three worker nodes dropped to near 0%. Crucial customer webhooks were completely stalled. The system wasn't struggling; it was dead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Mistake: Chasing Ghosts
&lt;/h2&gt;

&lt;p&gt;When a message queue freezes, you usually assume one of two things: a database deadlock, or a downstream API rate limit holding up the workers. &lt;/p&gt;

&lt;p&gt;I immediately checked our RDS monitoring. The database was completely idle. I checked our external payment gateways. They were responding in a snappy 50ms. &lt;/p&gt;

&lt;p&gt;Support was already getting tickets. Customers were asking why their webhooks weren’t firing. The pressure was building fast.&lt;/p&gt;

&lt;p&gt;Next stop: Sentry and Datadog. I filtered for application errors in the last hour. &lt;/p&gt;

&lt;p&gt;Nothing. Absolutely zero errors. &lt;/p&gt;

&lt;p&gt;The worker processes were simply restarting over and over again, silently failing. In a panic, we spun up two extra worker nodes, hoping to brute-force our way through whatever was clogging the pipes. The new workers instantly picked up the exact same tasks, crashed, and joined the restart loop. &lt;/p&gt;

&lt;p&gt;Scaling up didn't fix the problem. By the time we realized hardware wasn't the answer, the queue had been stuck for almost 40 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation: Going Off the Grid
&lt;/h2&gt;

&lt;p&gt;I realized our centralized observability tools were blind to whatever was killing the workers. It was time to go old-school. &lt;/p&gt;

&lt;p&gt;I SSH’d directly into one of the worker nodes, stopped the Celery daemon, and wrote a quick Python script to manually consume a single message from the RabbitMQ queue, bypassing our full application stack and middleware.&lt;/p&gt;

&lt;p&gt;To my surprise, the message processed perfectly. &lt;/p&gt;

&lt;p&gt;The database updated. The webhook fired. Why did it work flawlessly in my isolated script, but instantly kill the actual worker cluster? &lt;/p&gt;

&lt;p&gt;I dug into the raw, system-level container logs (&lt;code&gt;syslog&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt;). Buried deep in the terminal output, bypassing Datadog completely, I found this endless loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-08-24 10:01:12] ERROR/MainProcess: Task failed: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf0 in position 1023: unexpected end of data
[2026-08-24 10:01:12] WARNING/MainProcess: Rejecting task (requeue=True)
[2026-08-24 10:01:13] ERROR/MainProcess: Task failed: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf0 in position 1023: unexpected end of data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the exception happened inside a core middleware component and the worker process was dying so fast, the Sentry SDK never got a chance to flush the event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Killer: A $50 Cost-Saving Hack
&lt;/h2&gt;

&lt;p&gt;The traceback pointed to our custom Logging Middleware. &lt;/p&gt;

&lt;p&gt;A few weeks prior, to save money on Datadog ingestion costs, a developer had added a middleware that truncated the raw HTTP payload of any outgoing request to exactly 1024 bytes before logging it. To ensure they were strictly counting bytes (since Datadog bills by byte, not character), they wrote this exact line of code:&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="c1"&gt;# Truncate to 1024 bytes to save log costs
&lt;/span&gt;&lt;span class="n"&gt;truncated_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Outgoing webhook: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;truncated_payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked perfectly for weeks. Until a customer placed an order and included a ghost emoji (👻) in their checkout note. &lt;/p&gt;

&lt;p&gt;The ghost emoji &lt;code&gt;👻&lt;/code&gt; is exactly 4 bytes in UTF-8 (&lt;code&gt;\xf0\x9f\x91\xbb&lt;/code&gt;). The UTF-8 representation of that emoji happened to fall &lt;em&gt;exactly&lt;/em&gt; on the 1024th byte boundary of the payload. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[:1024]&lt;/code&gt; slice didn't care about characters. It brutally sliced the 4-byte UTF-8 character right down the middle, capturing only the first byte (&lt;code&gt;\xf0&lt;/code&gt;). When the middleware subsequently called &lt;code&gt;.decode('utf-8')&lt;/code&gt; on the sliced byte string, Python rightfully threw a &lt;code&gt;UnicodeDecodeError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Poison Pill
&lt;/h2&gt;

&lt;p&gt;Because this unhandled exception happened deep inside the logging layer, it instantly crashed the main Celery worker process. &lt;/p&gt;

&lt;p&gt;Here is where our architecture backfired:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The worker crashed &lt;em&gt;before&lt;/em&gt; it could send the &lt;code&gt;ACK&lt;/code&gt; (acknowledgment) to RabbitMQ.&lt;/li&gt;
&lt;li&gt;Because we had specifically configured &lt;code&gt;acks_late=True&lt;/code&gt; for reliability, RabbitMQ assumed the worker died mid-task and quickly requeued the message.&lt;/li&gt;
&lt;li&gt;Another worker immediately picked it up, tried to log it, and instantly crashed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our resilient architecture had completely backfired. The retry mechanism guaranteed that this single broken message would monopolize the entire cluster indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The immediate hotfix was a simple one-line change to gracefully handle byte splitting using &lt;code&gt;errors='ignore'&lt;/code&gt;. However, silently dropping broken characters isn't ideal for production logging. &lt;/p&gt;

&lt;p&gt;We quickly followed up with a proper UTF-8 safe truncation helper that respects character boundaries:&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;def&lt;/span&gt; &lt;span class="nf"&gt;safe_truncate_utf8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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="n"&gt;max_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;encoded&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;max_bytes&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;text&lt;/span&gt;

    &lt;span class="c1"&gt;# Truncate to max_bytes, then decode ignoring errors, 
&lt;/span&gt;    &lt;span class="c1"&gt;# and re-encode to drop any partial trailing characters cleanly.
&lt;/span&gt;    &lt;span class="n"&gt;safe_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;max_bytes&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&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;safe_bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;truncated_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safe_truncate_utf8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deployed the hotfix. &lt;/p&gt;

&lt;p&gt;Within 5 seconds, the single poison pill message was successfully swallowed and logged. The 15,000 backed-up messages drained perfectly in less than three minutes. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;A 4-byte ghost in a sea of millions of requests brought our entire pipeline to its knees. It was a harsh lesson: when slicing strings in Python, always respect the Unicode boundaries, or your queue will pay the price. &lt;/p&gt;

&lt;p&gt;Sometimes, the most catastrophic outages don't come from massive traffic spikes or database deadlocks. They come from a single byte, sliced in half, hiding in plain sight.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>backend</category>
    </item>
    <item>
      <title>The Silent 350-Second Killer: How an AWS NAT Gateway Cost Us $50k</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 23 Aug 2026 19:12:37 +0000</pubDate>
      <link>https://dev.to/tahosin/the-silent-350-second-killer-how-an-aws-nat-gateway-cost-us-50k-adk</link>
      <guid>https://dev.to/tahosin/the-silent-350-second-killer-how-an-aws-nat-gateway-cost-us-50k-adk</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;p&gt;It was 2:17 AM when the support Slack channel exploded. Three vendors had just been paid twice for their monthly bulk payouts. By morning, the number was twelve. By the end of the week, we had accidentally disbursed over $50,000 in duplicate payments. Our CFO was furious, and our banking partner was starting to ask uncomfortable questions.&lt;/p&gt;

&lt;p&gt;I had that familiar, sickening pit in my stomach—the kind you get when you realize the bug isn't a simple logic error, and the clock is ticking while real money keeps leaking.&lt;/p&gt;

&lt;p&gt;Our architecture was standard: Django, Celery, and PostgreSQL hosted on AWS. When a payout batch was approved, a Celery worker picked up the task and made a synchronous HTTP POST request to our legacy banking partner's API. We had explicitly set the &lt;code&gt;requests&lt;/code&gt; timeout to 10 minutes (&lt;code&gt;timeout=600&lt;/code&gt;) because the bank's maximum processing SLA was 8 minutes. We assumed we had a safe 2-minute buffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idempotency Trap
&lt;/h2&gt;

&lt;p&gt;The first thing we investigated was idempotency. Why was the bank processing retries as new payouts? I checked the codebase.&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;headers&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;X-Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BANK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was our first, embarrassing mistake. We were generating a new UUID directly inside the task execution instead of deriving it from the database record. When Celery retried a failed task, it generated a fresh key. The bank happily accepted it as a brand new batch.&lt;/p&gt;

&lt;p&gt;We quickly hotfixed this to use a deterministic hash based on the database ID (&lt;code&gt;uuid.uuid5&lt;/code&gt;). That stopped the bleeding, but it didn't give me any peace of mind. We had patched the symptom, not the disease. I still had absolutely no idea why the tasks were failing and retrying in the first place.&lt;/p&gt;

&lt;p&gt;We blamed PostgreSQL locks. I spent hours analyzing &lt;code&gt;pg_stat_activity&lt;/code&gt;, but the database was healthy.&lt;/p&gt;

&lt;p&gt;We blamed the bank. We got on a tense call with their engineers. They pulled up their logs and proved that they were receiving our requests and returning a &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;"Your endpoint is just incredibly slow," I argued. "Some of these batches take over 6 minutes."&lt;br&gt;
"Yes," their lead engineer replied. "It's a legacy mainframe. It takes 6 minutes. But we always return the success response."&lt;/p&gt;
&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;If the bank was sending the response, why was our Python worker reporting a &lt;code&gt;ReadTimeout&lt;/code&gt; after 10 minutes? Where was the data going?&lt;/p&gt;

&lt;p&gt;I stopped looking at application logs and started looking at the network. I SSHed into a production worker, installed &lt;code&gt;tcpdump&lt;/code&gt;, and waited for a large bulk payout to trigger.&lt;/p&gt;

&lt;p&gt;When it finally happened, I opened the packet capture in Wireshark. The transaction told a very specific story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:15:00.000 IP worker.local:43912 &amp;gt; bank.api.com:443: Flags [P.], seq 1:1500, ack 1, length 1500 (HTTP POST)
10:15:00.124 IP bank.api.com:443 &amp;gt; worker.local:43912: Flags [.], ack 1500 (TCP ACK)
... [ABSOLUTE SILENCE] ...
10:25:00.000 Python worker hits 600s timeout, closes socket.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bank acknowledged receiving our POST payload. Then, for exactly 10 minutes, there was dead silence. No response payload. No FIN. No RST.&lt;/p&gt;

&lt;p&gt;But wait. If the bank's mainframe finished processing the payout around the 6-minute mark, why didn't I see their HTTP 200 OK packet in my trace?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Killer
&lt;/h2&gt;

&lt;p&gt;Then I remembered our infrastructure diagram. Our worker nodes were in a private AWS subnet. All outbound traffic went through an AWS NAT Gateway.&lt;/p&gt;

&lt;p&gt;I dug into the AWS documentation and found the culprit: &lt;strong&gt;AWS NAT Gateways have a fixed idle timeout of 350 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The timeline matched perfectly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At 10:15:00, our worker sent the HTTP POST. The NAT Gateway tracked the connection.&lt;/li&gt;
&lt;li&gt;The bank's mainframe started processing. Because it was synchronous, no packets flowed in either direction while it chugged away.&lt;/li&gt;
&lt;li&gt;At 10:20:50 (exactly 350 seconds later), the NAT Gateway's idle timer expired. It silently deleted the connection state.&lt;/li&gt;
&lt;li&gt;At 10:21:15 (around the 6-minute mark), the bank finished and sent the HTTP 200 OK response.&lt;/li&gt;
&lt;li&gt;The NAT Gateway received the bank's packet. But since its state table had expired, it considered the packet invalid. It dropped the response and sent a TCP RST packet &lt;em&gt;back to the bank&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Our worker never saw that RST packet. It saw absolutely nothing. It just sat there, blindly waiting for data until its own 10-minute application timeout finally fired.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two systems silently disagreed about whether a TCP connection still existed. That disagreement cost us $50,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix wasn't an architectural overhaul. We just needed to trick the NAT Gateway into keeping the state alive.&lt;/p&gt;

&lt;p&gt;We needed TCP Keepalive. This is a low-level socket option that tells the Linux kernel to send an empty TCP probe packet if the connection is idle. By sending a probe before the 350-second mark, we could reset the NAT Gateway's timer.&lt;/p&gt;

&lt;p&gt;We injected the socket options directly into &lt;code&gt;urllib3&lt;/code&gt; via the &lt;code&gt;requests&lt;/code&gt; adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.adapters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPAdapter&lt;/span&gt;

&lt;span class="c1"&gt;# Enable keepalive, probe every 60 seconds
&lt;/span&gt;&lt;span class="n"&gt;keepalive_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOL_SOCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SO_KEEPALIVE&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="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IPPROTO_TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCP_KEEPIDLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IPPROTO_TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCP_KEEPINTVL&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IPPROTO_TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCP_KEEPCNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# We explicitly avoid max_retries here to prevent automatic replays on side-effecting payment operations
&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HTTPAdapter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poolmanager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection_pool_kw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;socket_options&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keepalive_options&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deployed the configuration and watched the metrics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timeout rate before the patch: 4.2% of bulk payouts.&lt;/li&gt;
&lt;li&gt;Timeout rate after the patch: 0%.&lt;/li&gt;
&lt;li&gt;Duplicate payments: 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are currently working with the bank to migrate this flow to an asynchronous webhook architecture so we never have to hold a connection open for 6 minutes again. I still get a small jolt of anxiety every time I see a long-running synchronous HTTP call in a pull request.&lt;/p&gt;

&lt;p&gt;The biggest takeaway for me? Never trust the network to close a connection for you. If you are forced to build a synchronous integration that takes minutes to complete, you have to defend your sockets. A few bytes of TCP Keepalive stabilized the system, but it was a harsh reminder of how leaky our abstractions really are.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>networking</category>
      <category>backend</category>
    </item>
    <item>
      <title>A simple typo swallowed our telemetry: Why our AI agent spans vanished into the void</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 23 Aug 2026 19:08:58 +0000</pubDate>
      <link>https://dev.to/tahosin/a-simple-typo-swallowed-our-telemetry-why-our-ai-agent-spans-vanished-into-the-void-4ckl</link>
      <guid>https://dev.to/tahosin/a-simple-typo-swallowed-our-telemetry-why-our-ai-agent-spans-vanished-into-the-void-4ckl</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;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;In the modern era of autonomous AI agents, observability is everything. If an agent hallucinates or takes an unexpected path, you need to know exactly which model was invoked, what the system prompt was, and how long it took. &lt;/p&gt;

&lt;p&gt;The Sentry Python SDK provides a fantastic integration (&lt;code&gt;OpenAIAgentsIntegration&lt;/code&gt;) specifically designed to trace the internal execution of the &lt;code&gt;openai&lt;/code&gt; Swarm/Agents framework. It hooks into the framework's core loops to automatically generate beautiful, hierarchical tracing spans for every tool execution and model invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;While reviewing the integration for version &lt;code&gt;0.8.0&lt;/code&gt; of the &lt;code&gt;openai-agents&lt;/code&gt; SDK, something felt off. The tool execution spans were working perfectly. However, the model invocation spans, which are the most critical pieces of the telemetry, were completely missing. &lt;/p&gt;

&lt;p&gt;The Sentry dashboard was virtually blind to when the agent actually reached out to the LLM. &lt;/p&gt;

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

&lt;p&gt;I started digging into the SDK's source code at &lt;code&gt;sentry_sdk/integrations/openai_agents/__init__.py&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;0.8.0&lt;/code&gt; release of the &lt;code&gt;openai-agents&lt;/code&gt; framework, the internal architecture had changed. The method responsible for resolving models was refactored and moved from &lt;code&gt;AgentRunner._get_model()&lt;/code&gt; to a new location: &lt;code&gt;agents.run_internal.turn_preparation.get_model()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Sentry maintainers were aware of this change. They wrote the exact code needed to wrap the new &lt;code&gt;turn_preparation.get_model&lt;/code&gt; function:&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="nd"&gt;@wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn_preparation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_wrapped_get_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents.Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents.RunConfig&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents.Model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_get_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn_preparation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrap was perfectly executed. The telemetry injection was flawless. So why wasn't it working? &lt;/p&gt;

&lt;p&gt;I looked at the very next line of code, where the wrapped function is injected back into the SDK:&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;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_wrapped_get_model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. A simple, silent typo. &lt;/p&gt;

&lt;p&gt;They correctly wrapped &lt;code&gt;turn_preparation.get_model&lt;/code&gt;, but they assigned the result to &lt;code&gt;run_loop.get_model&lt;/code&gt;. Because the SDK internally uses &lt;code&gt;turn_preparation&lt;/code&gt; to resolve the model, the patched function sitting inside &lt;code&gt;run_loop&lt;/code&gt; was completely ignored. &lt;/p&gt;

&lt;p&gt;The patch never installed. The model invocation spans vanished into the void.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;GitHub PR:&lt;/strong&gt; &lt;a href="https://github.com/getsentry/sentry-python/pull/7227" rel="noopener noreferrer"&gt;getsentry/sentry-python#7227&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix was as simple as correcting the assignment target. I submitted a patch to ensure the wrapped function overwrites the actual method the SDK uses:&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_wrapped_get_model&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;turn_preparation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_wrapped_get_model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By changing &lt;code&gt;run_loop&lt;/code&gt; to &lt;code&gt;turn_preparation&lt;/code&gt;, the SDK now correctly intercepts the model resolution step. &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%2Fpsjy3wswwxuxrb7wm94y.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%2Fpsjy3wswwxuxrb7wm94y.jpg" alt="Sentry Traces Restored" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Accurate telemetry restored!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In software engineering, some of the most critical bugs that entirely disable core features often come down to a single line of misdirected code.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>python</category>
      <category>sentry</category>
    </item>
    <item>
      <title>The tests were green but our async loops were doomed: Plugging a severe memory leak in Google GenAI</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:20:06 +0000</pubDate>
      <link>https://dev.to/tahosin/the-tests-were-green-but-our-async-loops-were-doomed-plugging-a-severe-memory-leak-in-google-genai-311g</link>
      <guid>https://dev.to/tahosin/the-tests-were-green-but-our-async-loops-were-doomed-plugging-a-severe-memory-leak-in-google-genai-311g</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;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Building AI applications that process thousands of text prompts requires speed and concurrency. When Google released their official &lt;code&gt;google-genai&lt;/code&gt; Python SDK, I immediately integrated it to run bulk asynchronous generation tasks. &lt;/p&gt;

&lt;p&gt;I wrote a loop that fired off hundreds of calls to &lt;code&gt;generate_content&lt;/code&gt; asynchronously. The logic was rock solid. The tests went green. The responses were flowing in fast.&lt;/p&gt;

&lt;p&gt;Then, out of nowhere, the application crashed. The server simply ran out of memory. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;I restarted the application and watched the system monitor. With every loop iteration, the RAM footprint grew larger and larger. Even after the LLM responses were fully processed, saved to the database, and the local variables went out of scope, the memory just kept climbing. &lt;/p&gt;

&lt;p&gt;The garbage collector wasn't cleaning up. I was facing a textbook, catastrophic memory leak.&lt;/p&gt;

&lt;h3&gt;
  
  
  The investigation
&lt;/h3&gt;

&lt;p&gt;I imported &lt;code&gt;tracemalloc&lt;/code&gt; to trace memory allocations deep inside the runtime. &lt;/p&gt;

&lt;p&gt;After running the loop for just 10 seconds, the profiler pointed its finger straight at the underlying HTTP clients: &lt;code&gt;httpx&lt;/code&gt; and &lt;code&gt;aiohttp&lt;/code&gt;. A massive accumulation of raw byte strings (&lt;code&gt;b""&lt;/code&gt;) and HTTP response objects was stubbornly clinging to the memory heap.&lt;/p&gt;

&lt;p&gt;I traced the bug down to &lt;code&gt;google/genai/_api_client.py&lt;/code&gt;. When a non-streaming asynchronous request completes, the SDK extracts the &lt;code&gt;.text&lt;/code&gt; payload from the network response and wraps it in a beautiful SDK model for the user. &lt;/p&gt;

&lt;p&gt;But there was a fatal omission: the SDK never explicitly called &lt;code&gt;.close()&lt;/code&gt; or &lt;code&gt;.release()&lt;/code&gt; on those underlying network objects. Because complex asynchronous connection pools (like &lt;code&gt;httpx.AsyncClient&lt;/code&gt;) often keep references to recent connections alive, those massive byte buffers were being held hostage in memory indefinitely. These buffers sometimes contained megabytes of JSON text.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;GitHub PR:&lt;/strong&gt; &lt;a href="https://github.com/googleapis/python-genai/pull/2900" rel="noopener noreferrer"&gt;googleapis/python-genai#2900&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Relying on Python's garbage collector in high-throughput async loops is a dangerous game. To fix this permanently, the SDK needed to manually obliterate the network buffers the moment the text payload was safely extracted. &lt;/p&gt;

&lt;p&gt;I submitted a patch to the core request engine to introduce an &lt;strong&gt;eager memory release pattern&lt;/strong&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="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;client_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aclose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aclose&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;client_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;client_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&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;client_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;client_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By manually setting &lt;code&gt;_content = b""&lt;/code&gt;, we aggressively break the reference to the massive byte strings. Even if the empty response shell is temporarily held alive by an async stack frame, the actual payload memory is instantly freed back to the system.&lt;/p&gt;

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

&lt;p&gt;After applying the patch, I pushed 10,000 generation requests through the system. The memory footprint remained completely flat. The leak was finally plugged!&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%2F42k45yt3w7czjek9xfd4.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%2F42k45yt3w7czjek9xfd4.jpg" alt="Memory flatline" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Memory usage completely flatlined after the fix!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>python</category>
      <category>google</category>
    </item>
    <item>
      <title>My React Search Box Froze on Every Keystroke. I Fixed It by Stealing Python's #1 Scope Rule.</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Tue, 07 Jul 2026 06:08:02 +0000</pubDate>
      <link>https://dev.to/tahosin/my-react-search-box-froze-on-every-keystroke-i-fixed-it-by-stealing-pythons-1-scope-rule-k2i</link>
      <guid>https://dev.to/tahosin/my-react-search-box-froze-on-every-keystroke-i-fixed-it-by-stealing-pythons-1-scope-rule-k2i</guid>
      <description>&lt;p&gt;One search box made my whole React dashboard feel broken.&lt;/p&gt;

&lt;p&gt;Every keypress landed late. Almost 300ms late.&lt;/p&gt;

&lt;p&gt;My first instinct was the same one a lot of React developers have: memoize everything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useCallback&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Wrap half the app in &lt;code&gt;React.memo&lt;/code&gt; and hope the smoke clears?&lt;/p&gt;

&lt;p&gt;That was my first instinct. It was also the wrong one.&lt;/p&gt;

&lt;p&gt;The app was not huge. It was a side-project dashboard with roughly 150 components: a sidebar, a header, a data table, a few charts, filters, settings panels, the usual little pieces that pile up faster than you expect.&lt;/p&gt;

&lt;p&gt;Then I typed into the search box.&lt;/p&gt;

&lt;p&gt;The input was almost 300ms behind my actual typing. Not "maybe I can feel it" slow. Properly annoying slow. The kind where you press backspace twice, wait, and then wonder if your laptop is having a private crisis.&lt;/p&gt;

&lt;p&gt;React DevTools made it obvious. One keystroke was lighting up the whole screen.&lt;/p&gt;

&lt;p&gt;The search box changed.&lt;/p&gt;

&lt;p&gt;The table changed.&lt;/p&gt;

&lt;p&gt;Also the header rendered again.&lt;/p&gt;

&lt;p&gt;The sidebar rendered again.&lt;/p&gt;

&lt;p&gt;The charts rendered again.&lt;/p&gt;

&lt;p&gt;Even the footer showed up in the profiler like it had important search-related business to attend to.&lt;/p&gt;

&lt;p&gt;It did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake was boring
&lt;/h2&gt;

&lt;p&gt;Here is a smaller version of what I had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useState&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"layout"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sidebar&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchBar&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataGrid&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HeavyCharts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;At first glance this does not look terrible. The search query belongs to the dashboard page, right?&lt;/p&gt;

&lt;p&gt;That was the trap.&lt;/p&gt;

&lt;p&gt;The state lived in &lt;code&gt;Dashboard&lt;/code&gt;, so every search change updated &lt;code&gt;Dashboard&lt;/code&gt;. When a component's state changes, React renders that component again and then renders through its children unless something bails out.&lt;/p&gt;

&lt;p&gt;So every keystroke asked the whole dashboard to participate.&lt;/p&gt;

&lt;p&gt;That is not React being weird. That is me putting a noisy piece of state in a place with a huge audience.&lt;/p&gt;

&lt;p&gt;The profiler before the fix looked like this:&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%2Fivbku3o0tzzpc3jhepg9.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%2Fivbku3o0tzzpc3jhepg9.png" alt="React Profiler before the fix, with the whole dashboard re-rendering and 142.7ms total render time" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Per keystroke, this render path was taking &lt;strong&gt;142.7ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a smooth 60 FPS interaction, you get around 16ms per frame. My search box was spending almost nine frames just rendering. No mystery there. It felt slow because it was slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python had already taught me the rule
&lt;/h2&gt;

&lt;p&gt;The funny part is that I already knew the principle. I just did not recognize it in React clothing.&lt;/p&gt;

&lt;p&gt;In Python, you learn pretty quickly not to throw state into a wider scope than it needs.&lt;/p&gt;

&lt;p&gt;If a variable is only used inside one function, keep it inside that function. If you make it module-level because "maybe something else will need it later", you usually buy yourself confusion. More places can touch it. More code has to care about it. Debugging gets louder.&lt;/p&gt;

&lt;p&gt;React state has the same smell.&lt;/p&gt;

&lt;p&gt;This is not a perfect one-to-one comparison. &lt;code&gt;useState&lt;/code&gt; inside &lt;code&gt;Dashboard&lt;/code&gt; is not literally a Python global. But it behaved like a value with too much reach. A tiny search string had enough placement power to wake up the entire page.&lt;/p&gt;

&lt;p&gt;So instead of asking, "How do I memoize all these components?"&lt;/p&gt;

&lt;p&gt;I asked a simpler question:&lt;/p&gt;

&lt;p&gt;Who actually needs &lt;code&gt;searchQuery&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The answer was embarrassingly small.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SearchBar&lt;/code&gt; needs it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DataGrid&lt;/code&gt; needs it.&lt;/p&gt;

&lt;p&gt;That is basically it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was one small wrapper
&lt;/h2&gt;

&lt;p&gt;I moved the state down into a component that only contains the search input and the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useState&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchableDataGrid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchBar&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataGrid&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"layout"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sidebar&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchableDataGrid&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HeavyCharts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;That was the whole change.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dashboard&lt;/code&gt; went back to being layout.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SearchableDataGrid&lt;/code&gt; became the owner of the noisy state.&lt;/p&gt;

&lt;p&gt;Now a keystroke renders &lt;code&gt;SearchableDataGrid&lt;/code&gt;, &lt;code&gt;SearchBar&lt;/code&gt;, and &lt;code&gt;DataGrid&lt;/code&gt;. It does not ask &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;Sidebar&lt;/code&gt;, &lt;code&gt;HeavyCharts&lt;/code&gt;, or &lt;code&gt;Footer&lt;/code&gt; to do anything.&lt;/p&gt;

&lt;p&gt;After the change, the profiler looked like this:&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%2Fd6warkeegwpa77prf2re.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%2Fd6warkeegwpa77prf2re.png" alt="React Profiler after the fix, with only SearchableDataGrid re-rendering and 4.1ms total render time" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Render time dropped from &lt;strong&gt;142.7ms to 4.1ms&lt;/strong&gt; per keystroke.&lt;/p&gt;

&lt;p&gt;That number is from my app on my machine, so do not read it as a magic promise. The important part is the shape of the fix: the render work stopped spreading across the page.&lt;/p&gt;

&lt;p&gt;Here is the difference visually:&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%2F6ce1bsogh21cimac6lw4.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%2F6ce1bsogh21cimac6lw4.png" alt="Component tree before and after the fix, showing the render blast radius shrinking from the whole dashboard to the search area" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here is the isolated version:&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%2Fepxepr5g64ztfwi66hmg.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%2Fepxepr5g64ztfwi66hmg.png" alt="State colocation diagram showing search state living inside SearchableDataGrid" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rule I took from it:&lt;/p&gt;

&lt;p&gt;If a piece of state changes often, do not put it higher than the components that actually need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why memoization was the wrong first move
&lt;/h2&gt;

&lt;p&gt;There is nothing wrong with &lt;code&gt;React.memo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is nothing wrong with &lt;code&gt;useMemo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is nothing wrong with &lt;code&gt;useCallback&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But in this case I was reaching for them too early.&lt;/p&gt;

&lt;p&gt;Memoization can help a component skip work when its inputs have not changed. It can be great for expensive children with stable props. But it also adds another thing your future self has to understand.&lt;/p&gt;

&lt;p&gt;The cleaner fix was to stop triggering those components in the first place.&lt;/p&gt;

&lt;p&gt;I now try to debug React performance in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did I put state too high?&lt;/li&gt;
&lt;li&gt;Is the expensive work happening during render?&lt;/li&gt;
&lt;li&gt;Can the expensive work be delayed or split?&lt;/li&gt;
&lt;li&gt;Is this finally a real memoization problem?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That order saves me from decorating bad structure with clever hooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this trick is not enough
&lt;/h2&gt;

&lt;p&gt;State colocation fixed my issue because the wasted work was outside the table.&lt;/p&gt;

&lt;p&gt;If your table itself is expensive, this will not magically save you.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;DataGrid&lt;/code&gt; filters 10,000 rows on every keypress, moving the state down still leaves you with a heavy table render. In that case, &lt;code&gt;useDeferredValue&lt;/code&gt; is often a better next step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useDeferredValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchableDataGrid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&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;deferredQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDeferredValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchBar&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setSearchQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataGrid&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;deferredQuery&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;That keeps the input responsive while React updates the heavier result view with a lower priority.&lt;/p&gt;

&lt;p&gt;There are also cases where the state really does need to live higher.&lt;/p&gt;

&lt;p&gt;If the header needs to show "23 matching results", and the sidebar needs to sync with the same query, pushing the state all the way down might make the data flow awkward. At that point I would consider lifting it back up, using context for a small shared area, or using a tiny external store if the page is getting messy.&lt;/p&gt;

&lt;p&gt;And yes, sometimes &lt;code&gt;React.memo&lt;/code&gt; is exactly the right tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&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;HeavyCharts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HeavyCharts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Expensive chart rendering that does not depend on searchQuery.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Charts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I just do not want it to be my first reflex anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tiny checklist I use now
&lt;/h2&gt;

&lt;p&gt;When a React interaction feels slow, I open the Profiler before changing code.&lt;/p&gt;

&lt;p&gt;I record the interaction once.&lt;/p&gt;

&lt;p&gt;Then I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed?&lt;/li&gt;
&lt;li&gt;Which components rendered?&lt;/li&gt;
&lt;li&gt;Which of those renders were actually needed?&lt;/li&gt;
&lt;li&gt;Is the slow part extra rendering, or expensive work inside the component that really did need to render?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the whole page lights up after one keypress, I look for state sitting too high in the tree.&lt;/p&gt;

&lt;p&gt;If only the right component lights up but it still takes 100ms, I look inside that component.&lt;/p&gt;

&lt;p&gt;That one distinction saves a lot of random hook experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that annoyed me
&lt;/h2&gt;

&lt;p&gt;The fix was not advanced.&lt;/p&gt;

&lt;p&gt;It was not a new library.&lt;/p&gt;

&lt;p&gt;It was not a clever hook.&lt;/p&gt;

&lt;p&gt;It was a boring scoping mistake.&lt;/p&gt;

&lt;p&gt;I had treated a search string like page-level state because it was convenient at the time. React did exactly what I asked it to do, and then I spent an hour blaming rendering.&lt;/p&gt;

&lt;p&gt;The Python version of the lesson would be:&lt;/p&gt;

&lt;p&gt;If a variable belongs in a function, do not put it in the module.&lt;/p&gt;

&lt;p&gt;The React version:&lt;/p&gt;

&lt;p&gt;If state belongs to two components, do not make the whole page carry it.&lt;/p&gt;

&lt;p&gt;That is the rule I am keeping.&lt;/p&gt;

&lt;p&gt;Have you had one of these "wait, the fix was that small?" performance bugs in React or another framework? I am especially curious about the ones where the profiler made you feel personally attacked.&lt;/p&gt;

</description>
      <category>react</category>
      <category>python</category>
      <category>beginners</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Chasing the Sun: Building a 3D AI-Powered Solstice Runner with React Three Fiber</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 21 Jun 2026 20:40:36 +0000</pubDate>
      <link>https://dev.to/tahosin/chasing-the-sun-building-a-3d-ai-powered-solstice-runner-with-react-three-fiber-bk3</link>
      <guid>https://dev.to/tahosin/chasing-the-sun-building-a-3d-ai-powered-solstice-runner-with-react-three-fiber-bk3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;For this game jam, I built &lt;strong&gt;Solstice Runner&lt;/strong&gt;, a 3D endless adventure where you literally chase the sun on the longest day of the year. &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%2Fmvividwwl7mthbklopw7.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%2Fmvividwwl7mthbklopw7.png" alt="Solstice Runner Title Screen" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea started with a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if the solstice was a race against time, guided by logic?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Solstice Runner, players navigate a vibrant 3D world, dodging obstacles and interacting with a dynamically generated environment. Instead of just making a standard runner, I wanted the environment and the interactions to feel alive. The game celebrates the June Solstice by having the player constantly move forward toward a sun that never seems to set, while solving on-the-fly logical puzzles inspired by Alan Turing.&lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a fast-paced 3D web experience that seamlessly blends action with intelligent logic.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Here is a quick video demo of the gameplay in action:&lt;/p&gt;

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

&lt;p&gt;You can also try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running through the 3D obstacle course&lt;/li&gt;
&lt;li&gt;Experiencing real-time dynamic AI dialogues&lt;/li&gt;
&lt;li&gt;Chasing the horizon of the longest day&lt;/li&gt;
&lt;li&gt;Testing your reflexes against Turing-inspired logic nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;... directly through the deployed game (hosted on GitHub Pages):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://x-tahosin.github.io/Solstice-Runner/" rel="noopener noreferrer"&gt;🎮 Play Solstice Runner Here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/x-tahosin/Solstice-Runner" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; — Frontend UI and application state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Three Fiber / Three.js&lt;/strong&gt; — The entire 3D rendering pipeline and game world&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; — Lightning-fast development and build tooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AI API (Gemini)&lt;/strong&gt; — Generating dynamic interactions and logic on the fly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — Styling the overlay HUDs&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I started by focusing on the core rendering loop:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Render Scene → Handle Movement → Generate AI Logic → Avoid Obstacles&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the 3D movement felt right in the browser, I started layering in everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the 3D World in the Browser
&lt;/h3&gt;

&lt;p&gt;One of the biggest challenges was getting 3D performance running smoothly in the browser. Using pure Three.js can require a lot of boilerplate, so I opted for React Three Fiber. It allowed me to treat 3D meshes, lights, and cameras as standard React components, which sped up development tremendously. The environment uses warm, bright lighting to reflect the summer solstice vibe.&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%2F2eoodq58upmko6d8njzb.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%2F2eoodq58upmko6d8njzb.png" alt="3D Environment in Solstice Runner" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating the Google AI API
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding the obstacles or dialog sequences, I used the Google AI API as the "Game Master". Every time a player reaches a specific checkpoint, the game pings the API with the current game state to generate unique logic challenges and narrative text. &lt;/p&gt;

&lt;p&gt;It took some tweaking to ensure the API calls didn't block the game's render loop, but the result is a game that responds dynamically. &lt;/p&gt;

&lt;h3&gt;
  
  
  A Nod to Alan Turing
&lt;/h3&gt;

&lt;p&gt;Since June is also Alan Turing's birth month, I wanted to include a tribute to him. The logic checkpoints you encounter while running are framed as "Turing Gates". They require quick thinking—such as pattern recognition or simple binary decisions—before you can continue your run. &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%2F26otjuoqa21boqvpo0co.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%2F26otjuoqa21boqvpo0co.png" alt="AI Forge Menu" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;The Mechanics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Turing Gates&lt;/td&gt;
&lt;td&gt;Logic Nodes&lt;/td&gt;
&lt;td&gt;Answer AI-generated logic puzzles to pass safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endless Day&lt;/td&gt;
&lt;td&gt;June Solstice&lt;/td&gt;
&lt;td&gt;The lighting stays perpetually at golden hour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;I am submitting this project for the &lt;strong&gt;Best Google AI Usage&lt;/strong&gt; and &lt;strong&gt;Best Ode to Alan Turing&lt;/strong&gt; categories. &lt;/p&gt;

&lt;p&gt;The Google AI API handles the core dynamic dialog and logic generation, making every run feel unique. Meanwhile, the core mechanics of navigating "Turing Gates" serve as a direct homage to Turing's legacy in computing and intelligence. &lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Discuss! 👇
&lt;/h2&gt;

&lt;p&gt;I'd love to hear your thoughts on mixing 3D web games with generative AI!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have you tried using React Three Fiber or the Gemini API in your own projects?&lt;/li&gt;
&lt;li&gt;What kind of AI-driven mechanics would you add to an endless runner?&lt;/li&gt;
&lt;li&gt;How far did you manage to run in the game? 🏃‍♂️&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a comment below, and let's chat about web game development, AI, or just share your high score! &lt;/p&gt;

&lt;p&gt;Thanks for playing!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I Replaced 200 Threads With 10,000. Java Finished 13.5x Faster.</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:51:01 +0000</pubDate>
      <link>https://dev.to/tahosin/i-started-10000-java-threads-my-laptop-barely-noticed-2i6g</link>
      <guid>https://dev.to/tahosin/i-started-10000-java-threads-my-laptop-barely-noticed-2i6g</guid>
      <description>&lt;p&gt;I expected the fans to spin.&lt;/p&gt;

&lt;p&gt;I had just asked Java to start &lt;strong&gt;10,000 tasks&lt;/strong&gt;, give each task its own virtual&lt;br&gt;
thread, and make every one wait for 100 milliseconds.&lt;/p&gt;

&lt;p&gt;Instead, the program finished before I could move my hand away from Enter.&lt;/p&gt;

&lt;p&gt;So I ran it again. Then three more times.&lt;/p&gt;

&lt;p&gt;On my 12-logical-processor laptop, the median result looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Executor&lt;/th&gt;
&lt;th&gt;10,000 waiting tasks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed pool of 200 platform threads&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,116 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One virtual thread per task&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;378 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is &lt;strong&gt;13.5x faster completion&lt;/strong&gt; after changing the executor, not the task.&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.amazonaws.com%2Fuploads%2Farticles%2F6u4fp3uadf5qqd4gmzk8.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.amazonaws.com%2Fuploads%2Farticles%2F6u4fp3uadf5qqd4gmzk8.png" alt="Benchmark results comparing platform and virtual threads" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not proof that virtual threads make Java code 13.5x faster.&lt;/p&gt;

&lt;p&gt;It is proof that I had been thinking about threads incorrectly.&lt;/p&gt;

&lt;p&gt;Let us rebuild that mental model from the inside.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, Make a Prediction
&lt;/h2&gt;

&lt;p&gt;Each task does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMillis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 10,000 tasks.&lt;/p&gt;

&lt;p&gt;How long should the whole program take?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A:&lt;/strong&gt; About 1,000 seconds, because &lt;code&gt;10,000 x 100 ms = 1,000 seconds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B:&lt;/strong&gt; About 5 seconds, because 200 platform threads process the work in waves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C:&lt;/strong&gt; Well under 1 second, because waiting virtual threads can step aside&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three answers can be correct. The executor decides which world you live&lt;br&gt;
in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Old Mental Model
&lt;/h2&gt;

&lt;p&gt;For most of Java's life, a Java thread was a thin wrapper around an operating&lt;br&gt;
system thread.&lt;/p&gt;

&lt;p&gt;That made threads useful, but expensive enough to treat as a limited resource.&lt;/p&gt;

&lt;p&gt;If your server had a pool of 200 platform threads and all 200 were waiting for&lt;br&gt;
a slow database, request 201 had to stand in line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; platform thread -&amp;gt; OS thread -&amp;gt; wait
request -&amp;gt; platform thread -&amp;gt; OS thread -&amp;gt; wait
request -&amp;gt;       queue       -&amp;gt;          -&amp;gt; wait for a free thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code was blocked, but the operating system thread assigned to it was still&lt;br&gt;
occupied.&lt;/p&gt;

&lt;p&gt;Virtual threads break that one-to-one relationship.&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.amazonaws.com%2Fuploads%2Farticles%2F2ctp8cglum3dwayrbcjr.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.amazonaws.com%2Fuploads%2Farticles%2F2ctp8cglum3dwayrbcjr.png" alt="Platform threads compared with virtual threads" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A virtual thread is still a real &lt;code&gt;java.lang.Thread&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The difference is that it does not permanently own an OS thread. The JVM&lt;br&gt;
schedules many virtual threads onto a smaller number of platform threads,&lt;br&gt;
called &lt;strong&gt;carrier threads&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can see the distinction directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofPlatform&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentThread&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isVirtual&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofVirtual&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentThread&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isVirtual&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;virtual&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;false
true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;Thread&lt;/code&gt; API. Different scheduling model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When a Virtual Thread Waits?
&lt;/h2&gt;

&lt;p&gt;Imagine a virtual thread running on carrier thread 3.&lt;/p&gt;

&lt;p&gt;It calls a supported blocking operation, such as &lt;code&gt;Thread.sleep()&lt;/code&gt; or blocking&lt;br&gt;
network I/O.&lt;/p&gt;

&lt;p&gt;The JVM can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pause the virtual thread.&lt;/li&gt;
&lt;li&gt;Unmount it from carrier thread 3.&lt;/li&gt;
&lt;li&gt;Use carrier thread 3 to run other virtual threads.&lt;/li&gt;
&lt;li&gt;Remount the original virtual thread when its wait is over.&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fedd4vam2dr97i4n76z7y.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.amazonaws.com%2Fuploads%2Farticles%2Fedd4vam2dr97i4n76z7y.png" alt="Timeline showing a virtual thread stepping aside while waiting" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The virtual thread did not make the database, network, or timer faster.&lt;/p&gt;

&lt;p&gt;It stopped wasting a scarce carrier thread while waiting.&lt;/p&gt;

&lt;p&gt;That sentence is the whole feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Virtual threads make &lt;strong&gt;waiting&lt;/strong&gt; cheap. They do not make &lt;strong&gt;work&lt;/strong&gt; cheap.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The Experiment
&lt;/h2&gt;

&lt;p&gt;Here is the important part of the benchmark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="no"&gt;TASKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt; &lt;span class="no"&gt;WAIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMillis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExecutorService&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="no"&gt;TASKS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;TASKS&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;taskId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WAIT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;taskId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran the same method with two executors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newFixedThreadPool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first executor lets at most 200 tasks wait at once.&lt;/p&gt;

&lt;p&gt;The virtual-thread executor starts one virtual thread for every task. When the&lt;br&gt;
tasks sleep, the JVM can unmount them and keep its carrier threads available.&lt;/p&gt;

&lt;p&gt;That is why the fixed pool behaves roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000 tasks / 200 threads = 50 waves
50 waves x 100 ms          = about 5 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The virtual-thread version does not need 50 waves. Almost every task can begin,&lt;br&gt;
sleep, and get out of the carriers' way.&lt;/p&gt;

&lt;p&gt;The measured medians from three runs were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WAITING WORK
200 platform threads        5,116 ms
virtual thread per task       378 ms

CPU WORK
platform threads            2,387 ms
virtual threads             2,300 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The waiting result changed dramatically.&lt;/p&gt;

&lt;p&gt;The CPU result did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benchmark Trap
&lt;/h2&gt;

&lt;p&gt;Virtual threads are not tiny turbo buttons.&lt;/p&gt;

&lt;p&gt;To test that, I also submitted 48 CPU-heavy tasks that counted primes up to&lt;br&gt;
1,000,000.&lt;/p&gt;

&lt;p&gt;Both executors finished in roughly the same time because my laptop still had&lt;br&gt;
only 12 logical processors.&lt;/p&gt;

&lt;p&gt;You can create one million virtual threads.&lt;/p&gt;

&lt;p&gt;You cannot create one million CPU cores.&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.amazonaws.com%2Fuploads%2Farticles%2Fvcfvj07axuus4y301gtw.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.amazonaws.com%2Fuploads%2Farticles%2Fvcfvj07axuus4y301gtw.png" alt="Decision tree for choosing virtual threads" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good virtual-thread workloads spend meaningful time waiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP requests&lt;/li&gt;
&lt;li&gt;database queries&lt;/li&gt;
&lt;li&gt;many file operations, after profiling&lt;/li&gt;
&lt;li&gt;message queues&lt;/li&gt;
&lt;li&gt;remote API calls&lt;/li&gt;
&lt;li&gt;many independent &lt;code&gt;sleep()&lt;/code&gt; or timer waits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poor candidates spend most of their time calculating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image processing&lt;/li&gt;
&lt;li&gt;video encoding&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;machine-learning inference&lt;/li&gt;
&lt;li&gt;large in-memory transformations&lt;/li&gt;
&lt;li&gt;number crunching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CPU-bound work, use bounded parallelism near the amount of CPU your machine&lt;br&gt;
can actually execute.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Simplest Useful Rule
&lt;/h2&gt;

&lt;p&gt;When tasks mostly wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadUser&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadOrders&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="n"&gt;renderProfile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is ordinary, blocking, and readable.&lt;/p&gt;

&lt;p&gt;That is intentional.&lt;/p&gt;

&lt;p&gt;For years, developers often had to choose between simple thread-per-request&lt;br&gt;
code that did not scale and asynchronous code that scaled but split the&lt;br&gt;
workflow across callbacks, futures, or reactive operators.&lt;/p&gt;

&lt;p&gt;Virtual threads make the simple shape practical for many high-throughput&lt;br&gt;
blocking applications.&lt;/p&gt;

&lt;p&gt;They do not remove every concurrency problem. They remove one expensive&lt;br&gt;
assumption: that every concurrent task needs its own OS thread.&lt;/p&gt;
&lt;h2&gt;
  
  
  Do Not Pool Virtual Threads
&lt;/h2&gt;

&lt;p&gt;This feels wrong at first.&lt;/p&gt;

&lt;p&gt;We learned to pool threads because platform threads were expensive. A pool&lt;br&gt;
limited how many of those scarce threads existed.&lt;/p&gt;

&lt;p&gt;Virtual threads are designed to be created per task.&lt;/p&gt;

&lt;p&gt;So this is the normal pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a tiny pool of reusable virtual threads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must limit access to something scarce, limit &lt;strong&gt;that thing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose a partner API permits only 20 concurrent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt; &lt;span class="n"&gt;partnerApiSlots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;callPartnerApi&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;InterruptedException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;partnerApiSlots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;makeBlockingHttpRequest&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;partnerApiSlots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbmj39g8cw3r2wlr0izs.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.amazonaws.com%2Fuploads%2Farticles%2Flbmj39g8cw3r2wlr0izs.png" alt="Many virtual threads passing through a semaphore before a partner API" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The executor can still create a virtual thread per task.&lt;/p&gt;

&lt;p&gt;The semaphore protects the actual bottleneck.&lt;/p&gt;

&lt;p&gt;This separation is useful far beyond virtual threads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Concurrency is how much work can be in progress. Capacity is how much work a&lt;br&gt;
dependency can safely accept.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Quiet &lt;code&gt;ThreadLocal&lt;/code&gt; Trap
&lt;/h2&gt;

&lt;p&gt;Virtual threads support &lt;code&gt;ThreadLocal&lt;/code&gt;, so request context such as a user ID or&lt;br&gt;
trace ID can continue to work.&lt;/p&gt;

&lt;p&gt;The dangerous pattern is using &lt;code&gt;ThreadLocal&lt;/code&gt; as a tiny object pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpensiveClient&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CLIENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withInitial&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ExpensiveClient:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may look efficient when 200 pooled platform threads reuse 200 clients.&lt;/p&gt;

&lt;p&gt;With one virtual thread per task, it can quietly become thousands of expensive&lt;br&gt;
clients that are barely reused.&lt;/p&gt;

&lt;p&gt;Keep context in thread-local variables only when it truly belongs to the task.&lt;br&gt;
Do not use them to cache heavy reusable objects per virtual thread.&lt;/p&gt;
&lt;h2&gt;
  
  
  You Can Observe Them
&lt;/h2&gt;

&lt;p&gt;Virtual threads are invisible to the operating system because the OS sees&lt;br&gt;
carrier threads, not every virtual thread.&lt;/p&gt;

&lt;p&gt;The JDK understands them, though.&lt;/p&gt;

&lt;p&gt;You can create a virtual-thread-aware dump with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jcmd &amp;lt;pid&amp;gt; Thread.dump_to_file &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json threads.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters during debugging. An OS dashboard may show a modest&lt;br&gt;
thread count while the JVM is managing thousands of virtual threads.&lt;/p&gt;

&lt;p&gt;The right question is not only "how many threads exist?"&lt;/p&gt;

&lt;p&gt;It is "what are those threads waiting for?"&lt;/p&gt;
&lt;h2&gt;
  
  
  One Outdated Warning
&lt;/h2&gt;

&lt;p&gt;You may have read this advice:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never block inside &lt;code&gt;synchronized&lt;/code&gt; code when using virtual threads, because it&lt;br&gt;
pins the carrier thread.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That warning mattered when virtual threads became final in Java 21.&lt;/p&gt;

&lt;p&gt;Java 24 changed the implementation through&lt;br&gt;
&lt;a href="https://openjdk.org/jeps/491" rel="noopener noreferrer"&gt;JEP 491&lt;/a&gt;. Virtual threads can now release their&lt;br&gt;
carrier when blocking inside &lt;code&gt;synchronized&lt;/code&gt; code in the normal case.&lt;/p&gt;

&lt;p&gt;Pinning has not vanished completely. Native and foreign-function calls can&lt;br&gt;
still pin a virtual thread.&lt;/p&gt;

&lt;p&gt;But the blanket "virtual threads and &lt;code&gt;synchronized&lt;/code&gt; do not mix" rule is&lt;br&gt;
outdated on modern JDKs.&lt;/p&gt;

&lt;p&gt;This is one reason I ran the experiment on Java 25 LTS instead of repeating an&lt;br&gt;
old Java 21 checklist.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Five-Minute Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Do not rewrite an application because virtual threads sound exciting.&lt;/p&gt;

&lt;p&gt;Take one blocking workflow and inspect it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the workload waits.&lt;/strong&gt; Look for database calls, HTTP calls, file
access, queues, and sleeps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the task executor.&lt;/strong&gt; Try
&lt;code&gt;Executors.newVirtualThreadPerTaskExecutor()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep downstream limits.&lt;/strong&gt; Connection pools, API quotas, and rate limits
still exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load test the real path.&lt;/strong&gt; A sleep benchmark teaches the model, not your
production capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure CPU and memory too.&lt;/strong&gt; Cheap threads can still run expensive code
or retain large objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check native integrations.&lt;/strong&gt; Native calls are one of the remaining
pinning cases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not "use virtual threads everywhere."&lt;/p&gt;

&lt;p&gt;The goal is "stop paying for idle OS threads where you do not need them."&lt;/p&gt;
&lt;h2&gt;
  
  
  The Mental Model I Am Keeping
&lt;/h2&gt;

&lt;p&gt;Before this experiment, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;More concurrent Java work requires a larger thread pool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Waiting work wants cheap virtual threads. CPU work wants bounded&lt;br&gt;
parallelism. Scarce dependencies want explicit limits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That model is simple enough for a beginner and accurate enough to prevent a&lt;br&gt;
surprising number of production mistakes.&lt;/p&gt;

&lt;p&gt;The full runnable lab behind the numbers uses only the JDK. No framework, build&lt;br&gt;
tool, or dependency is required.&lt;/p&gt;

&lt;p&gt;Compile and run it with Java 25:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;javac VirtualThreadsLab.java
java VirtualThreadsLab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="//./code/VirtualThreadsLab.java"&gt;Open the complete runnable &lt;code&gt;VirtualThreadsLab.java&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Virtual threads became final in Java 21. Java 25 is not required for the basic&lt;br&gt;
API, but it gives us the current LTS behavior, including the post-Java-24&lt;br&gt;
improvements discussed above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openjdk.org/jeps/444" rel="noopener noreferrer"&gt;JEP 444: Virtual Threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/java/javase/25/core/virtual-threads.html" rel="noopener noreferrer"&gt;Oracle Java 25 Guide: Virtual Threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openjdk.org/jeps/491" rel="noopener noreferrer"&gt;JEP 491: Synchronize Virtual Threads without Pinning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What should I put through this lab next: a database connection pool, 10,000&lt;br&gt;
real HTTP calls, or a &lt;code&gt;ThreadLocal&lt;/code&gt;-heavy application?&lt;/p&gt;

</description>
      <category>java</category>
      <category>beginners</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I Added a 71-Line Black Box to My Python Agent, Then Queried the $200 Crash With DuckDB</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Sun, 31 May 2026 16:54:50 +0000</pubDate>
      <link>https://dev.to/tahosin/i-added-a-71-line-black-box-to-my-python-agent-then-queried-the-200-crash-with-duckdb-4h18</link>
      <guid>https://dev.to/tahosin/i-added-a-71-line-black-box-to-my-python-agent-then-queried-the-200-crash-with-duckdb-4h18</guid>
      <description>&lt;p&gt;The incident started with a boring support automation task.&lt;/p&gt;

&lt;p&gt;Take a user request, search a private document index, summarize the answer, and hand the result to a reviewer. Nothing heroic. The kind of Python agent you build when the demo is over and the real workflow begins.&lt;/p&gt;

&lt;p&gt;Then one run got stuck in a retry loop.&lt;/p&gt;

&lt;p&gt;It did not burn $200 before I caught it. The actual test run was cheaper. The problem was the projection: same bad loop, same document search, same model calls, left inside the overnight batch. The estimate landed close to $200 for one avoidable failure.&lt;/p&gt;

&lt;p&gt;The answer it produced looked polished enough to pass a sleepy review. The trace behind it was not polished at all. The agent had called the right tool with the wrong input, retried against stale context, summarized old results, and kept paying for each turn.&lt;/p&gt;

&lt;p&gt;That is when I stopped treating the agent like a chat feature.&lt;/p&gt;

&lt;p&gt;I started treating it like a system that needs a black box.&lt;/p&gt;

&lt;p&gt;Not a dashboard. Not a full observability stack. Not another hosted service.&lt;/p&gt;

&lt;p&gt;Just one local file that can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did the agent try?&lt;/li&gt;
&lt;li&gt;Which tool did it call?&lt;/li&gt;
&lt;li&gt;What input did the tool receive?&lt;/li&gt;
&lt;li&gt;Did the tool fail?&lt;/li&gt;
&lt;li&gt;How long did it take?&lt;/li&gt;
&lt;li&gt;Did the run cross a cost or turn limit?&lt;/li&gt;
&lt;li&gt;Can I query the run after everything is over?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will build that black box in plain Python, then use DuckDB to inspect it like a tiny crash database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before And After
&lt;/h2&gt;

&lt;p&gt;Before the fix, debugging looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The final answer is wrong.
The model probably hallucinated.
Maybe the search tool returned bad data.
Maybe the retry loop reused an old message.
Maybe the cost spike came from the model call.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not debugging. That is guessing with syntax highlighting.&lt;/p&gt;

&lt;p&gt;After the fix, debugging looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn 1 called search_docs with the wrong query.
The tool timed out after 147.82 ms.
The retry used stale context.
The guard stopped the run at $0.0124.
DuckDB shows one tool_error and one guard_stop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same bug. Very different day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape Of The Problem
&lt;/h2&gt;

&lt;p&gt;A normal Python script usually fails in one place.&lt;/p&gt;

&lt;p&gt;An agent fails across a chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request -&amp;gt; Model Decision -&amp;gt; Tool Call -&amp;gt; Tool Result -&amp;gt; Next Turn -&amp;gt; Final Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;If you only log the final answer, you have a diary entry.&lt;/p&gt;

&lt;p&gt;If you record the chain, you have evidence.&lt;/p&gt;

&lt;p&gt;The simplest useful format is JSONL. One event per line.&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tool_start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"search_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"rate limits"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tool_end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"search_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;83.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"turn_end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&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="nl"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.0041&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;JSONL is boring in exactly the right way. It appends cleanly, survives crashes better than one large JSON document, and can be searched with normal tools.&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.amazonaws.com%2Fuploads%2Farticles%2Fvg78cujupv6jfmmkrz0y.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.amazonaws.com%2Fuploads%2Farticles%2Fvg78cujupv6jfmmkrz0y.png" alt="JSONL trace from a failed run" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Recorder That Does Real Work
&lt;/h2&gt;

&lt;p&gt;Here is the recorder.&lt;/p&gt;

&lt;p&gt;It does four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gives every run a unique id&lt;/li&gt;
&lt;li&gt;writes append-only JSONL events&lt;/li&gt;
&lt;li&gt;measures tool duration&lt;/li&gt;
&lt;li&gt;sanitizes obvious secrets before writing anything to disk
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;


&lt;span class="n"&gt;SECRET_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(api[_-]?key|token|password|secret|authorization|cookie)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sanitize&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;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&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&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;SECRET_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&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="n"&gt;cleaned&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[redacted]&lt;/span&gt;&lt;span class="sh"&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;cleaned&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;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;cleaned&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&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="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;item&lt;/span&gt; &lt;span class="ow"&gt;in&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;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentBlackBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&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="n"&gt;exist_ok&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&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;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="o"&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;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_exc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ok&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="n"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sanitize()&lt;/code&gt; function is not perfect. It is a seatbelt, not a vault.&lt;/p&gt;

&lt;p&gt;Still, it prevents the most embarrassing version of this pattern: building a helpful debug trace that quietly stores API keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap One Tool First
&lt;/h2&gt;

&lt;p&gt;Start with one tool. Do not instrument everything on day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&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;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Document search timed out&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSONL works well for append-only traces.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context managers are useful around tool calls.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DuckDB can query JSON files without a server.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now record the call:&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;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentBlackBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traces/run.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python agent trace format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&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_count&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;docs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;traces/run.jsonl&lt;/code&gt; and the key is redacted.&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="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"search_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"python agent trace format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"api_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[redacted]"&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;That tiny detail matters. Debugging should not create a second incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add A Cheap Run Guard
&lt;/h2&gt;

&lt;p&gt;Most runaway agent stories start with a loop that looked harmless.&lt;/p&gt;

&lt;p&gt;So the black box should not only record what happened. It should record when it refused to continue.&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;class&lt;/span&gt; &lt;span class="nc"&gt;RunStopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentBlackBox&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="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&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;turn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_turns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&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;RunStopped&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;Stopped at turn &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Max turns is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_turns&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="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spent_usd&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;RunStopped&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;Stopped at $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Budget is $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not exact billing. Use your provider response for real token counts when you have them.&lt;/p&gt;

&lt;p&gt;The goal here is a local tripwire. You want the run to leave a clear reason when it stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Tiny Agent Loop
&lt;/h2&gt;

&lt;p&gt;This fake loop keeps the moving parts small.&lt;/p&gt;

&lt;p&gt;Replace the pretend model section with your real model call.&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;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&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;input_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0000005&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0000015&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentBlackBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traces/run.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;max_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;

    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_usd&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;turn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;max_turns&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;stop_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turn_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_count&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;messages&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Pretend the model picked this tool input.
&lt;/span&gt;        &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&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;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python jsonl duckdb traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;

        &lt;span class="n"&gt;turn_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&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;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&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;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;turn_cost&lt;/span&gt;

        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turn_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message_count&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;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;turn_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;total_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Record every tool call as JSONL, then query failures after the run.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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;answer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once with a normal question.&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How should I debug Python agent tools?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it with a bad one.&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout during document search&lt;/span&gt;&lt;span class="sh"&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 second run should fail, but now it fails with a trail.&lt;/p&gt;

&lt;p&gt;To force a budget stop for testing, temporarily set &lt;code&gt;max_usd = 0.0001&lt;/code&gt;. The next guard check will write a &lt;code&gt;guard_stop&lt;/code&gt; event instead of letting the loop continue quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query The Crash With DuckDB
&lt;/h2&gt;

&lt;p&gt;This is the part that makes JSONL feel less like logging and more like a debugging tool.&lt;/p&gt;

&lt;p&gt;Install DuckDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query the trace:&lt;br&gt;
&lt;/p&gt;

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


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traces/run.jsonl&lt;/span&gt;&lt;span class="sh"&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&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;
        create or replace view events as
        select *
        from read_json_auto(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;);
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        select type, count(*) as events
        from events
        group by type
        order by events desc;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tool errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        select
            data.tool as tool,
            data.error_type as error_type,
            data.error as error,
            data.duration_ms as duration_ms
        from events
        where type = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool_error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Slow tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        select
            data.tool as tool,
            data.duration_ms as duration_ms
        from events
        where type = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool_end&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
        order by data.duration_ms desc
        limit 5;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run:&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="nf"&gt;query_trace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff should look something like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fnl76xqp1z7xokjcssrol.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.amazonaws.com%2Fuploads%2Farticles%2Fnl76xqp1z7xokjcssrol.png" alt="DuckDB query output for an agent crash" width="800" height="461"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event counts
+-------------+--------+
| type        | events |
+-------------+--------+
| guard_check |      4 |
| turn_start  |      3 |
| tool_start  |      3 |
| tool_end    |      2 |
| tool_error  |      1 |
| guard_stop  |      1 |
+-------------+--------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the crash row is now a query result, not a mystery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool errors
+-------------+--------------+---------------------------+-------------+
| tool        | error_type   | error                     | duration_ms |
+-------------+--------------+---------------------------+-------------+
| search_docs | TimeoutError | Document search timed out |      147.82 |
+-------------+--------------+---------------------------+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can answer questions that normal print logs make annoying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tools failed most often?&lt;/li&gt;
&lt;li&gt;Which tool was slowest?&lt;/li&gt;
&lt;li&gt;Which turn crossed the budget?&lt;/li&gt;
&lt;li&gt;Did the same input fail repeatedly?&lt;/li&gt;
&lt;li&gt;Did the guard stop the run, or did the tool crash first?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the upgrade.&lt;/p&gt;

&lt;p&gt;Not "I have logs."&lt;/p&gt;

&lt;p&gt;"I can interrogate the run."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Record In A Real Project
&lt;/h2&gt;

&lt;p&gt;For a demo, the trace above is enough.&lt;/p&gt;

&lt;p&gt;For a real project, I would add these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prompt_hash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool_schema_version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;input_tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output_tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;finish_reason&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retry_count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user_id_hash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not record these by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw access tokens&lt;/li&gt;
&lt;li&gt;private documents&lt;/li&gt;
&lt;li&gt;full customer prompts&lt;/li&gt;
&lt;li&gt;full tool responses with sensitive data&lt;/li&gt;
&lt;li&gt;cookies or request headers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boring security rule is simple:&lt;/p&gt;

&lt;p&gt;Record enough to debug behavior. Do not record enough to harm someone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern In One Sentence
&lt;/h2&gt;

&lt;p&gt;Every agent run should produce a local, append-only event stream that is safe to keep, easy to query, and useful after the process crashes.&lt;/p&gt;

&lt;p&gt;That sentence is less exciting than a new prompt trick.&lt;/p&gt;

&lt;p&gt;It is also more likely to save your weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full File
&lt;/h2&gt;

&lt;p&gt;Here is the complete example in one place.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;


&lt;span class="n"&gt;SECRET_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(api[_-]?key|token|password|secret|authorization|cookie)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sanitize&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;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[redacted]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;SECRET_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&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="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&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;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;list&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="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;item&lt;/span&gt; &lt;span class="ow"&gt;in&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;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentBlackBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&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="n"&gt;exist_ok&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&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;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="o"&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;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_exc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ok&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="n"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RunStopped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentBlackBox&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="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&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;turn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_turns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&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;RunStopped&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;Stopped at turn &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Max turns is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_turns&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="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard_stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spent_usd&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;RunStopped&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;Stopped at $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Budget is $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&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;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&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;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Document search timed out&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSONL works well for append-only traces.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context managers are useful around tool calls.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DuckDB can query JSON files without a server.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&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;input_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0000005&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0000015&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentBlackBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traces/run.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;max_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;

    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_usd&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;turn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;max_turns&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;stop_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turn_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_count&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;messages&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&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;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python jsonl duckdb traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-not-a-real-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;

        &lt;span class="n"&gt;turn_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&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;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&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;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;spent_usd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;turn_cost&lt;/span&gt;

        &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turn_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message_count&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;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;turn_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;total_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Record every tool call as JSONL, then query failures after the run.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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;answer&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How should I debug Python agent tools?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one line in that full file worth staring at:&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line changes the posture of the program.&lt;/p&gt;

&lt;p&gt;The run is no longer a private conversation with a model. It is a recorded execution with a trace you can inspect, query, and improve.&lt;/p&gt;

&lt;p&gt;That is the difference between a demo and something you can trust.&lt;/p&gt;

&lt;p&gt;What would you add next: prompt hashes, token counts, screenshots, checkpoints, or replayable tool fixtures?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>debugging</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Most Underrated Announcement from Google I/O 2026 Was Buried in a 90-Second Demo</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Thu, 21 May 2026 20:22:22 +0000</pubDate>
      <link>https://dev.to/tahosin/the-most-underrated-announcement-from-google-io-2026-was-buried-in-a-90-second-demo-550</link>
      <guid>https://dev.to/tahosin/the-most-underrated-announcement-from-google-io-2026-was-buried-in-a-90-second-demo-550</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-io-writing-2026-05-19"&gt;Google I/O Writing Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I watched the Google I/O 2026 keynote twice.&lt;/p&gt;

&lt;p&gt;First time, I got swept up in the shiny stuff. Gemini 3.5 Flash benchmarks. Veo 3 generating videos that look disturbingly real. Gemini Omni doing that multimodal physics thing. Cool. Expected. The usual I/O sugar rush that gets 50,000 retweets and fades by Thursday.&lt;/p&gt;

&lt;p&gt;Second time through, I caught something different.&lt;/p&gt;

&lt;p&gt;About 40 minutes into the developer keynote, sandwiched between the Jules GA announcement and a Stitch demo, there was maybe 90 seconds on something called the &lt;strong&gt;Managed Agents API&lt;/strong&gt;. The presenter dropped one line that made me hit pause and rewind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Deploy an autonomous agent that reasons, writes code, browses the web, and executes in a secure sandbox. One API call."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I closed every other tab. Pulled up the docs. Started writing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 19-Day Problem
&lt;/h2&gt;

&lt;p&gt;Here's some context. If you've tried building anything with AI agents in the past year, you know the drill. And by "drill" I mean "weeks of suffering."&lt;/p&gt;

&lt;p&gt;Say you want an agent that takes a GitHub issue, reads the codebase, writes a fix, runs tests, and opens a PR. Sounds straightforward, right? In reality, you're wiring up five services, spinning up sandboxed containers, managing auth, building tool-call routing, writing health checks, and setting up network policies so your agent doesn't accidentally nuke production at 3am on a Saturday.&lt;/p&gt;

&lt;p&gt;Last month I built an internal bot that triages support tickets. Took three weeks. The actual AI logic? One day. The other 19 days were pure infrastructure. Docker config. Sandbox isolation with gVisor. Network policies. Timeout handling. Health checks. Retry logic.&lt;/p&gt;

&lt;p&gt;Nineteen days of plumbing. One day of thinking.&lt;/p&gt;

&lt;p&gt;That ratio is broken. And this API just fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Weeks to Eleven Lines
&lt;/h2&gt;

&lt;p&gt;I took that same support ticket bot and rewired it on the Managed Agents API. Not a demo version. The same bot. Same capabilities.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;interaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;antigravity-preview-05-2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&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;You are a support ticket triage agent. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the following ticket, classify its severity, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identify the affected component from the codebase, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and draft a response with a proposed fix.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&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;Ticket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticket_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eleven lines. No Docker. No Kubernetes. No sandbox config.&lt;/p&gt;

&lt;p&gt;The API spins up a fresh, isolated Linux environment, loads the agent runtime, runs your task, hands back the result, and destroys the sandbox. Done.&lt;/p&gt;

&lt;p&gt;Here's what that looked like in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Old Setup&lt;/th&gt;
&lt;th&gt;Managed Agents API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 weeks&lt;/td&gt;
&lt;td&gt;1 afternoon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines of infra code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2,400&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines of agent logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker, gVisor, Redis, nginx&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google-genai&lt;/code&gt; pip package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance burden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container updates, health checks, scaling&lt;/td&gt;
&lt;td&gt;None (Google's problem)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I stared at my screen for a solid minute when it worked. Not because the output was flawless (it wasn't). Because I'd just thrown away three weeks of infrastructure code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Actually Built Under the Hood
&lt;/h2&gt;

&lt;p&gt;When you hit &lt;code&gt;interactions.create&lt;/code&gt;, four things happen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sandbox provisioning.&lt;/strong&gt; Google fires up an isolated Linux VM. Fresh filesystem every time. No leftover state from previous runs. Network access is off by default, opt-in only. This alone used to cost me a week of Docker and gVisor wrestling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent harness boots up.&lt;/strong&gt; This is the exact same runtime that powers Jules and the Antigravity desktop app. Not a watered-down version. Same thing. Every improvement Google makes to Jules? Your managed agents get it too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning loop.&lt;/strong&gt; The agent reads your input, builds a plan, starts executing. Writing files. Running code. Hitting the web if you've turned that on. There's a "critic" layer baked in that catches logic errors before returning output. Think of it like a built-in code reviewer that runs before every response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup.&lt;/strong&gt; Interaction finishes, sandbox gets nuked, you get the result plus any files the agent created. Thirty seconds to a few minutes total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Sandbox Breaks: The Preview Limitations
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this is ready for production. Two days of testing surfaced real problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout wall.&lt;/strong&gt; I pointed it at a 15,000-line codebase and asked it to refactor one module. Hit the 5-minute ceiling and died. Large, complex tasks choke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero memory between calls.&lt;/strong&gt; Each interaction gets a clean sandbox. Great for security. Terrible if you need your agent to remember context. You have to manage state yourself, passing the &lt;code&gt;previous_interaction_id&lt;/code&gt; and relevant context back in on every subsequent call. Not hard, but not free either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "preview" tax.&lt;/strong&gt; Pre-GA. Google says don't feed it sensitive data. Side projects and internal tools? Go for it. Customer data in production? Wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing is a black box.&lt;/strong&gt; Free during preview. Nobody knows what this costs at scale. That's a real problem for anyone planning production workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network access is half-baked.&lt;/strong&gt; Your agent can browse the public web. But reaching internal APIs? You need an MCP server as a bridge, which brings back some of that infrastructure overhead. A bit ironic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Stacks Up Against the Competition
&lt;/h2&gt;

&lt;p&gt;Here's what made me pay attention. Right now, if you want an autonomous agent that executes in a sandbox, your options are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI Assistants API&lt;/strong&gt; gives you code execution in a sandbox, but it's tied to OpenAI models, the sandbox is limited (no arbitrary binary execution, no web browsing), and you're paying per-token plus tool-call fees. It's also not truly "deploy an agent" so much as "run a conversation with tools."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic's tool-use&lt;/strong&gt; is powerful for single-turn tool calling, but there's no managed sandbox. You bring your own execution environment. So you're back to the Docker-and-gVisor dance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangGraph Cloud&lt;/strong&gt; gets you agent orchestration, but again, you manage the infrastructure. The execution environment is your problem.&lt;/p&gt;

&lt;p&gt;Google's approach is different. They're saying: give us the instructions, we'll handle the sandbox, the execution, the security, the cleanup. You don't think about infrastructure at all. That's a genuinely new position in this space.&lt;/p&gt;

&lt;p&gt;This is the first time a major cloud provider is treating autonomous agents as serverless compute, not just chat-with-tools.&lt;/p&gt;

&lt;p&gt;The tradeoff? You're locked into Google's ecosystem. The agent runs on Gemini models. If you need Claude or GPT-4 for a specific task, this isn't your tool. But for teams already in the Google stack, the friction drop is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feature That Actually Got Me: Saved Agents
&lt;/h2&gt;

&lt;p&gt;One-shot interactions are cool. But &lt;code&gt;agents.create&lt;/code&gt; is where things get interesting.&lt;/p&gt;

&lt;p&gt;You define an agent with custom instructions, specific tools, MCP connections, and environment settings. Save that whole configuration. Then trigger it by ID from anywhere. Cron job. Webhook. GitHub Action. Another agent.&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;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticket-triage-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_instruction&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;You are a senior support engineer. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classify tickets by severity. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Always check error logs before suggesting a fix. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never suggest restarting the service as a first option.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tools&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;code_execution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_browse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;environment_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;sandbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remote&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Trigger from anywhere
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&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;New ticket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticket_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wired one to our Slack. Someone files a bug, the agent auto-triages, pulls relevant logs, posts analysis in the thread. Forty lines of Python and a webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lambda Moment
&lt;/h2&gt;

&lt;p&gt;Remember 2014? Before Lambda, running code in the cloud meant EC2 instances. Load balancers. Auto-scaling groups. The works.&lt;/p&gt;

&lt;p&gt;Lambda said: give us the function, we handle the rest. People called it a toy. Then it ate the backend world.&lt;/p&gt;

&lt;p&gt;I keep seeing the same pattern. Before this API, building an agent meant managing infrastructure. Now you hand over instructions and Google runs the thing in a sandboxed environment.&lt;/p&gt;

&lt;p&gt;Maybe I'm wrong. Maybe this stays niche. But the parallel keeps nagging at me, and I haven't been able to talk myself out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Want to Build Next
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;docs drift detector&lt;/strong&gt; that points at a repo, reads the README, runs the code, and flags where documentation and behavior have diverged. Every project has this problem. Nobody fixes it manually.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;dependency changelog reader&lt;/strong&gt; that actually reads changelogs for your deps, understands breaking changes, and tells you which updates are safe to auto-merge and which ones need human review.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;pre-review PR agent&lt;/strong&gt; that reads changes before a human reviewer opens the PR, checks test coverage on modified files, identifies risky diffs, and writes review notes. Like a thorough junior dev who never sleeps.&lt;/p&gt;

&lt;p&gt;All of these would've been multi-week projects before. Now they're afternoon builds. That's the shift. Not what agents can do. But how fast you can ship them.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Now
&lt;/h2&gt;

&lt;p&gt;Google I/O 2026 had no shortage of headlines. Gemini 3.5 Flash is fast. Veo 3 is wild. Gemini Omni understanding physics makes you wonder what 2027 looks like.&lt;/p&gt;

&lt;p&gt;But this quiet little API is the one that actually changed my Tuesday. It didn't make me go "wow." It made me delete code. And that's usually how the important stuff starts.&lt;/p&gt;

&lt;p&gt;Open the docs. Write eleven lines of Python. See what happens.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? A reaction helps others find it too. Questions about the API or building with it? I'm in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googleiochallenge</category>
      <category>devchallenge</category>
      <category>discuss</category>
      <category>python</category>
    </item>
    <item>
      <title>Hermes Just Killed OpenClaw (Here's Why)</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Tue, 19 May 2026 13:12:33 +0000</pubDate>
      <link>https://dev.to/tahosin/hermes-just-killed-openclaw-heres-why-4c23</link>
      <guid>https://dev.to/tahosin/hermes-just-killed-openclaw-heres-why-4c23</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hermes-agent-2026-05-15"&gt;Hermes Agent Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I do not think OpenClaw is dead.&lt;/p&gt;

&lt;p&gt;That title is deliberately dramatic because the shift is dramatic. OpenClaw did something important: it made a lot of developers believe that a personal AI assistant could be more than a chat box. It could sit on your machine, connect to your messages, call tools, browse, run commands, and actually move work forward.&lt;/p&gt;

&lt;p&gt;But Hermes Agent changes the question.&lt;/p&gt;

&lt;p&gt;OpenClaw asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if I could run a personal AI assistant on my own devices?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hermes asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if my agent could live on my infrastructure, remember how I work, improve its own procedures, use tools across channels, and become more useful every week?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second question is why Hermes feels like the next step.&lt;/p&gt;

&lt;p&gt;Not because OpenClaw is bad. OpenClaw is popular for a reason. The official repo describes it as a personal AI assistant that runs on your own devices, answers through the channels you already use, and uses a Gateway as the control plane. That is a strong idea.&lt;/p&gt;

&lt;p&gt;The problem is that the AI agent market is moving from "assistant I operate" to "worker I supervise." Once that happens, the winning system is not the one with the loudest demo. It is the one with the better memory model, execution boundary, skill lifecycle, tool surface, and deployment story.&lt;/p&gt;

&lt;p&gt;That is where Hermes starts to pull ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;If I had to explain the difference in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OpenClaw feels like a local-first assistant. Hermes feels like agent infrastructure that happens to chat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A real agent has to do more than respond. It needs to run somewhere reliable. It needs to work while I am away. It needs to remember the parts of my environment that matter. It needs to learn repeatable procedures. It needs to make tool use safer, especially when those tools touch files, browsers, credentials, APIs, and servers.&lt;/p&gt;

&lt;p&gt;OpenClaw helped prove the demand.&lt;/p&gt;

&lt;p&gt;Hermes is making the operating model more serious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five claims that matter
&lt;/h2&gt;

&lt;p&gt;The loudest Hermes pitch right now is simple: install it, connect it, give it skills, run it on a server, and let it become your agent.&lt;/p&gt;

&lt;p&gt;That pitch is exciting, but I would not judge Hermes by hype. I would judge it by which claims survive contact with architecture.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;My read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"One-command install"&lt;/td&gt;
&lt;td&gt;Agents die when setup is fragile. If the first hour is dependency pain, most people quit.&lt;/td&gt;
&lt;td&gt;Useful, but not the real moat. Setup gets you to day one. Memory and skills decide day thirty.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Run it on a VPS or sandbox"&lt;/td&gt;
&lt;td&gt;A serious agent should not need your personal laptop open all day.&lt;/td&gt;
&lt;td&gt;This is one of Hermes' strongest arguments. Persistent agents belong on persistent infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Built-in skills"&lt;/td&gt;
&lt;td&gt;Skills turn vague AI behavior into repeatable procedures.&lt;/td&gt;
&lt;td&gt;Strong, especially because Hermes treats skills as something the agent can improve, not just something a user installs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Messaging integrations"&lt;/td&gt;
&lt;td&gt;Telegram, Discord, Slack, WhatsApp, and similar channels make the agent reachable from normal life.&lt;/td&gt;
&lt;td&gt;Important, but only if paired with background sessions. Otherwise it is just another bot in another inbox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Safer execution"&lt;/td&gt;
&lt;td&gt;Agents touch terminals, files, browsers, APIs, and credentials. That is dangerous by default.&lt;/td&gt;
&lt;td&gt;This is where Hermes feels more mature: command approval, allowlists, Docker, SSH, sandbox backends, and scoped toolsets all matter.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the lens for the rest of this post.&lt;/p&gt;

&lt;p&gt;I do not care whether Hermes can produce a flashy demo once. Most agent frameworks can do that now.&lt;/p&gt;

&lt;p&gt;I care whether Hermes has the bones for repeated work: memory, procedural learning, sandboxed execution, remote availability, and enough tool scoping to avoid turning convenience into a security incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenClaw won attention first
&lt;/h2&gt;

&lt;p&gt;OpenClaw's strength is obvious from its own README. It is broad, local, channel-heavy, and familiar to developers who want an assistant they can own.&lt;/p&gt;

&lt;p&gt;The official repo highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, Matrix, LINE, WeChat, and many more channels&lt;/li&gt;
&lt;li&gt;A local-first Gateway that owns messaging surfaces and routes requests&lt;/li&gt;
&lt;li&gt;First-class tools for browser, files, exec, canvas, cron, sessions, image generation, video generation, TTS, and sub-agents&lt;/li&gt;
&lt;li&gt;Skills based on &lt;code&gt;SKILL.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native onboarding with &lt;code&gt;openclaw onboard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Companion apps and nodes for macOS, iOS, Android, and headless devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not small. That is why OpenClaw became a reference point for personal agents.&lt;/p&gt;

&lt;p&gt;It also has a massive community. At the time I checked the GitHub API, OpenClaw had far more stars than Hermes. Popularity alone does not decide technical direction, but it does tell you something: OpenClaw made the category legible.&lt;/p&gt;

&lt;p&gt;For context, I checked the public repos directly: &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;openclaw/openclaw&lt;/a&gt; and &lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;NousResearch/hermes-agent&lt;/a&gt;. OpenClaw has the bigger gravity right now. Hermes has the more interesting agent-runtime thesis.&lt;/p&gt;

&lt;p&gt;The issue is that popularity also brings a harsh spotlight. Once strangers, groups, plugins, browsers, shells, and personal accounts all meet inside one assistant, the security model becomes the product.&lt;/p&gt;

&lt;p&gt;OpenClaw's own security docs are honest about this. The guidance assumes a personal assistant trust boundary: one trusted operator boundary per gateway. It says OpenClaw is not a hostile multi-tenant security boundary for adversarial users sharing one gateway. It also says the product default for trusted single-operator setups allows host execution in the &lt;code&gt;gateway&lt;/code&gt; or &lt;code&gt;node&lt;/code&gt; context unless you tighten it.&lt;/p&gt;

&lt;p&gt;That is not a cheap criticism. It is the tradeoff OpenClaw chose: powerful local assistant first, hardening second.&lt;/p&gt;

&lt;p&gt;Hermes starts from a different center.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hermes is built around compounding
&lt;/h2&gt;

&lt;p&gt;The most important Hermes idea is not Telegram integration. It is not browser automation. It is not even the tool count.&lt;/p&gt;

&lt;p&gt;The key idea is compounding.&lt;/p&gt;

&lt;p&gt;Hermes describes itself as a self-improving agent with a built-in learning loop. Its docs talk about agent-curated memory, autonomous skill creation, skill improvement during use, session search, external memory providers, and user modeling.&lt;/p&gt;

&lt;p&gt;That sounds abstract until you translate it into developer terms:&lt;/p&gt;

&lt;p&gt;If the agent solves a hard workflow today, it should not rediscover that workflow next week.&lt;/p&gt;

&lt;p&gt;That is the difference between a chatbot with tools and an agent that grows.&lt;/p&gt;

&lt;p&gt;Hermes has two memory layers that are easy to reason about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MEMORY.md&lt;/code&gt; for environment facts, project conventions, lessons learned, and workflow notes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER.md&lt;/code&gt; for preferences, communication style, expectations, and profile details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are bounded on purpose. Hermes keeps them focused instead of stuffing an infinite pile of text into every prompt. For older conversations, it uses SQLite session storage with FTS5 search and summarization.&lt;/p&gt;

&lt;p&gt;That design feels practical. The always-loaded memory stays small. The deeper history is searchable when needed.&lt;/p&gt;

&lt;p&gt;This is exactly how I want a serious agent to behave. I do not want it to remember everything equally. I want it to remember what changes future behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skill system is the real "DNA"
&lt;/h2&gt;

&lt;p&gt;Skills are where Hermes becomes interesting.&lt;/p&gt;

&lt;p&gt;OpenClaw has skills too. Its docs explain that skills are AgentSkills-compatible &lt;code&gt;SKILL.md&lt;/code&gt; folders that teach the agent how to use tools. OpenClaw loads bundled skills, managed/local skills, personal skills, project skills, and workspace skills.&lt;/p&gt;

&lt;p&gt;Hermes takes the same basic idea and pushes it closer to procedural memory.&lt;/p&gt;

&lt;p&gt;The Hermes docs say the agent can create, update, and delete its own skills through &lt;code&gt;skill_manage&lt;/code&gt;. It creates skills after complex successful tasks, when it finds the path through errors, when a user corrects its approach, or when it discovers a non-trivial workflow.&lt;/p&gt;

&lt;p&gt;That is the part that matters.&lt;/p&gt;

&lt;p&gt;Not "skills as a plugin folder."&lt;/p&gt;

&lt;p&gt;Skills as the agent writing down how to be better next time.&lt;/p&gt;

&lt;p&gt;This is the difference between installing extensions and building organizational memory. A good senior developer does not just solve an incident. They improve the runbook. Hermes is trying to make the agent do the same thing.&lt;/p&gt;

&lt;p&gt;And it is not only local skills. Hermes supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Official optional skills&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skills.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Well-known skill endpoints&lt;/li&gt;
&lt;li&gt;Direct URL skills&lt;/li&gt;
&lt;li&gt;GitHub skill installs&lt;/li&gt;
&lt;li&gt;Community registries&lt;/li&gt;
&lt;li&gt;External read-only skill directories&lt;/li&gt;
&lt;li&gt;Security scanning and audit commands for installed hub skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives Hermes a useful middle ground. It can learn locally, but it can also participate in a broader open skill ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The execution story is stronger
&lt;/h2&gt;

&lt;p&gt;This is where the comparison gets practical.&lt;/p&gt;

&lt;p&gt;An agent that can run commands should make you slightly nervous. That is healthy.&lt;/p&gt;

&lt;p&gt;Hermes treats terminal execution as a configurable backend. Commands can run locally, in Docker, over SSH, in Singularity, in Modal, in Daytona, or in Vercel Sandbox. The docs are clear about the tradeoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local is easy, but has no isolation&lt;/li&gt;
&lt;li&gt;Docker gives container isolation&lt;/li&gt;
&lt;li&gt;SSH moves execution to another server&lt;/li&gt;
&lt;li&gt;Modal and Daytona give cloud sandbox options&lt;/li&gt;
&lt;li&gt;Vercel Sandbox gives microVM-style cloud execution with snapshot persistence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security page goes further. With Docker, Hermes applies hardened container flags: drop capabilities, no new privileges, PID limits, tmpfs mounts, and explicit resource limits. It also avoids forwarding host environment variables by default.&lt;/p&gt;

&lt;p&gt;That matters for one simple reason:&lt;/p&gt;

&lt;p&gt;The agent should not automatically inherit your entire laptop just because you wanted it to scrape a page or refactor a file.&lt;/p&gt;

&lt;p&gt;OpenClaw can sandbox too. Its README points to Docker, SSH, and OpenShell options, and it recommends sandboxing for non-main sessions. Its security docs are detailed and serious.&lt;/p&gt;

&lt;p&gt;But the default mental model is different.&lt;/p&gt;

&lt;p&gt;OpenClaw is a personal assistant with optional hardening.&lt;/p&gt;

&lt;p&gt;Hermes is an agent runtime where isolated execution is part of the normal deployment conversation.&lt;/p&gt;

&lt;p&gt;That is why I would rather run Hermes on a VPS or cloud sandbox for always-on work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Messaging is not the win. Remote agency is.
&lt;/h2&gt;

&lt;p&gt;Both tools can talk through messaging platforms.&lt;/p&gt;

&lt;p&gt;OpenClaw has a huge channel list. Hermes also supports a wide set: Telegram, Discord, Slack, WhatsApp, Signal, SMS, Email, Matrix, Mattermost, Home Assistant, DingTalk, Feishu/Lark, WeCom, Microsoft Teams, and more.&lt;/p&gt;

&lt;p&gt;The interesting Hermes feature is not that you can message it.&lt;/p&gt;

&lt;p&gt;The interesting feature is that messaging becomes a control surface for background work.&lt;/p&gt;

&lt;p&gt;Hermes supports background sessions from messaging platforms. You can start a separate task, keep chatting in the main thread, and receive the result back in the same channel. That is a small feature on paper, but it changes the feel of the system.&lt;/p&gt;

&lt;p&gt;It stops being:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am chatting with a bot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am dispatching work to an agent that lives somewhere else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the future I care about.&lt;/p&gt;

&lt;p&gt;I do not want my personal agent trapped inside the laptop I am currently using. I want it on a server, reachable from my phone, able to run a long task, report back, and remember the result.&lt;/p&gt;

&lt;p&gt;Hermes is built for that shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool breadth is now table stakes
&lt;/h2&gt;

&lt;p&gt;There was a time when "this agent can browse the web and run commands" sounded wild.&lt;/p&gt;

&lt;p&gt;That time is over.&lt;/p&gt;

&lt;p&gt;Both OpenClaw and Hermes have serious tool surfaces.&lt;/p&gt;

&lt;p&gt;OpenClaw ships built-in tools for shell execution, code execution, browser control, web search, file I/O, patching, messaging, canvas, nodes, cron, images, music, video, TTS, sessions, and sub-agents.&lt;/p&gt;

&lt;p&gt;Hermes ships a broad registry too: web search, extraction, terminal, file editing, browser automation, vision, image generation, TTS, memory, session search, cron, messaging, delegation, code execution, Home Assistant, MCP tools, RL tools, and more.&lt;/p&gt;

&lt;p&gt;So the question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which one has tools?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which one makes tools safer, more composable, and easier to scope per situation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hermes has a clear toolset model. Toolsets can be enabled per session, per platform, or per task. There are platform presets like &lt;code&gt;hermes-cli&lt;/code&gt;, &lt;code&gt;hermes-telegram&lt;/code&gt;, and dynamic MCP toolsets. That gives you a cleaner way to say:&lt;/p&gt;

&lt;p&gt;"This Telegram agent can do X, but not Y."&lt;/p&gt;

&lt;p&gt;For me, that is more important than raw tool count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hermes vs OpenClaw
&lt;/h2&gt;

&lt;p&gt;Here is my practical comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;OpenClaw&lt;/th&gt;
&lt;th&gt;Hermes Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core identity&lt;/td&gt;
&lt;td&gt;Personal AI assistant&lt;/td&gt;
&lt;td&gt;Self-improving agent runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental model&lt;/td&gt;
&lt;td&gt;Local-first Gateway assistant&lt;/td&gt;
&lt;td&gt;Persistent worker on your infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;CLI onboarding and Gateway daemon&lt;/td&gt;
&lt;td&gt;CLI, Gateway, and multiple runtime backends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messaging&lt;/td&gt;
&lt;td&gt;Very broad channel coverage&lt;/td&gt;
&lt;td&gt;Channels plus background sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Skills loaded from many locations&lt;/td&gt;
&lt;td&gt;Skills as procedural memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Workspace and session context&lt;/td&gt;
&lt;td&gt;Curated memory plus session search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooling&lt;/td&gt;
&lt;td&gt;Broad built-in tools&lt;/td&gt;
&lt;td&gt;Toolsets, MCP, delegation, media, web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Personal trust boundary, hardening available&lt;/td&gt;
&lt;td&gt;Approval, isolation, env filtering, scoped tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Device or Gateway host&lt;/td&gt;
&lt;td&gt;Local, VPS, Docker, SSH, Modal, Daytona, Vercel Sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal user&lt;/td&gt;
&lt;td&gt;Power user with a device assistant&lt;/td&gt;
&lt;td&gt;Developer building a supervised digital worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biggest risk&lt;/td&gt;
&lt;td&gt;Too much power in one assistant boundary&lt;/td&gt;
&lt;td&gt;Newer ecosystem still proving itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is why I do not read Hermes as "another OpenClaw clone."&lt;/p&gt;

&lt;p&gt;Hermes is competing on a different axis.&lt;/p&gt;

&lt;p&gt;OpenClaw made the assistant powerful.&lt;/p&gt;

&lt;p&gt;Hermes is trying to make the assistant compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical playbook
&lt;/h2&gt;

&lt;p&gt;If you are reading this and wondering "okay, but what do I actually try first?", this is the path I would take.&lt;/p&gt;

&lt;p&gt;First, run Hermes somewhere disposable. A local machine is fine for learning, but the interesting path is Docker, SSH, Modal, Daytona, or another sandbox backend. The whole point is to avoid giving an experimental agent unlimited access to your daily machine on day one.&lt;/p&gt;

&lt;p&gt;Then connect one messaging surface, not five. Telegram or Discord is enough. Make sure allowlists or DM pairing are enabled before you give the agent terminal access.&lt;/p&gt;

&lt;p&gt;Then give Hermes one recurring workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/background Research the latest Hermes Agent docs changes, summarize the developer impact, and send me 5 possible DEV post angles.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, watch for the compounding moment. If the workflow takes several tool calls, has a repeatable structure, or needs a correction from you, that is exactly the kind of thing that should become a skill.&lt;/p&gt;

&lt;p&gt;A good first Hermes skill would not be "write blog posts." Too vague.&lt;/p&gt;

&lt;p&gt;A better one would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;research-release-notes

When given a GitHub repo or docs page:
1. Find the latest release or docs update.
2. Prefer primary sources.
3. Extract concrete changes.
4. Separate confirmed facts from opinion.
5. Produce a DEV-ready outline with links.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is where Hermes becomes more than a chat assistant. You are not just asking it to do a task. You are teaching it a durable way to do that class of task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where OpenClaw still wins
&lt;/h2&gt;

&lt;p&gt;A good comparison should admit the other side.&lt;/p&gt;

&lt;p&gt;OpenClaw still has big advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It has enormous attention and community gravity.&lt;/li&gt;
&lt;li&gt;Its channel ecosystem is very broad.&lt;/li&gt;
&lt;li&gt;Its native app and node story is compelling.&lt;/li&gt;
&lt;li&gt;Its local-first assistant feel is easier to explain to non-agent people.&lt;/li&gt;
&lt;li&gt;It has already shaped how people talk about personal AI assistants.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your goal is "I want a personal AI assistant connected to my messaging apps and devices," OpenClaw is still a serious answer.&lt;/p&gt;

&lt;p&gt;But if your goal is "I want an agent that can become operational infrastructure," Hermes is the more interesting answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Hermes wins
&lt;/h2&gt;

&lt;p&gt;Hermes wins because it is opinionated about the hard parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It treats memory as a product surface
&lt;/h3&gt;

&lt;p&gt;Memory is not just chat history. It is a curated behavioral layer. The split between &lt;code&gt;MEMORY.md&lt;/code&gt;, &lt;code&gt;USER.md&lt;/code&gt;, and searchable session history is simple enough to trust and flexible enough to grow.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It treats skills as learning
&lt;/h3&gt;

&lt;p&gt;The agent can create and update skills after hard tasks. That is the closest thing to compounding engineering knowledge in this category.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It treats execution location as a first-class choice
&lt;/h3&gt;

&lt;p&gt;Local, Docker, SSH, Modal, Daytona, Vercel Sandbox, Singularity. That is not a footnote. That is the difference between a toy assistant and something you can deploy with intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It treats messaging as dispatch
&lt;/h3&gt;

&lt;p&gt;I can talk to the agent through Telegram or Discord, but the real value is sending background work and getting results back. That makes the chat app a command center, not the product itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. It treats safety as architecture, not a disclaimer
&lt;/h3&gt;

&lt;p&gt;Allowlists, DM pairing, command approval, container isolation, MCP credential filtering, context scanning, env var filtering, and scoped toolsets are not glamorous features. They are the features you need after the first impressive demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger point
&lt;/h2&gt;

&lt;p&gt;The agent space is splitting into two philosophies.&lt;/p&gt;

&lt;p&gt;One philosophy says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give the user a powerful assistant and let them connect everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The other says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give the user an agent runtime that can be supervised, isolated, taught, remembered, and deployed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenClaw represents the first philosophy extremely well.&lt;/p&gt;

&lt;p&gt;Hermes represents the second.&lt;/p&gt;

&lt;p&gt;That is why I think Hermes is the more important project to study right now.&lt;/p&gt;

&lt;p&gt;OpenClaw proved people want agents with hands.&lt;/p&gt;

&lt;p&gt;Hermes is asking what happens when those hands also get memory, runbooks, safer execution, background work, and a home outside your current laptop.&lt;/p&gt;

&lt;p&gt;That is the jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build with Hermes
&lt;/h2&gt;

&lt;p&gt;If I were turning this into a real project, I would build a developer publishing agent.&lt;/p&gt;

&lt;p&gt;Not a blog spammer. A proper assistant for technical writing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Watch official docs, GitHub releases, and challenge pages.&lt;/li&gt;
&lt;li&gt;Summarize what changed with links to primary sources.&lt;/li&gt;
&lt;li&gt;Keep a memory of my writing preferences and recurring projects.&lt;/li&gt;
&lt;li&gt;Create reusable skills for research, outline creation, source checking, and DEV formatting.&lt;/li&gt;
&lt;li&gt;Draft posts in my style, but keep claims grounded in citations.&lt;/li&gt;
&lt;li&gt;Send drafts to Telegram for review.&lt;/li&gt;
&lt;li&gt;Track comments and suggest follow-up posts based on real discussion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That would use the Hermes shape well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long-running background research&lt;/li&gt;
&lt;li&gt;web extraction&lt;/li&gt;
&lt;li&gt;session search&lt;/li&gt;
&lt;li&gt;persistent memory&lt;/li&gt;
&lt;li&gt;skills that improve over time&lt;/li&gt;
&lt;li&gt;messaging delivery&lt;/li&gt;
&lt;li&gt;scoped tool access&lt;/li&gt;
&lt;li&gt;scheduled tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the kind of workflow where Hermes makes more sense than a one-shot chat assistant.&lt;/p&gt;

&lt;p&gt;The point is not that Hermes can write.&lt;/p&gt;

&lt;p&gt;The point is that Hermes can build a writing operation around memory, tools, and feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;Did Hermes literally kill OpenClaw?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;OpenClaw is too useful, too popular, and too culturally important to dismiss.&lt;/p&gt;

&lt;p&gt;But Hermes may have killed the idea that a personal agent is only a local assistant with a chat interface.&lt;/p&gt;

&lt;p&gt;That is the real shift.&lt;/p&gt;

&lt;p&gt;The next generation of agents will not be judged only by how many apps they connect to. They will be judged by whether they can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remember the right things&lt;/li&gt;
&lt;li&gt;forget the wrong things&lt;/li&gt;
&lt;li&gt;learn procedures&lt;/li&gt;
&lt;li&gt;run in isolated environments&lt;/li&gt;
&lt;li&gt;work asynchronously&lt;/li&gt;
&lt;li&gt;integrate with open tools&lt;/li&gt;
&lt;li&gt;stay useful after the first demo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By that standard, Hermes is not just another agent.&lt;/p&gt;

&lt;p&gt;It is a strong argument for where agent software is going next.&lt;/p&gt;

&lt;p&gt;That is my real test for any agent framework now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does it get more useful because I used it yesterday?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, it is still mostly a tool wrapper.&lt;/p&gt;

&lt;p&gt;If the answer is yes, we are finally talking about agent software.&lt;/p&gt;

&lt;p&gt;And yes, that is why the title says it:&lt;/p&gt;

&lt;p&gt;Hermes just killed OpenClaw.&lt;/p&gt;

&lt;p&gt;Not by replacing it overnight.&lt;/p&gt;

&lt;p&gt;By making the category grow up.&lt;/p&gt;

&lt;p&gt;The first thing I would personally validate is not whether Hermes can write a pretty paragraph. It is whether a Docker or SSH-backed Hermes research agent can run for a week, keep useful memory, and avoid turning one bad tool call into a machine-level mess. If you have tried either backend already, I would genuinely like to hear which one felt smoother and where it broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/challenges/hermes-agent-2026-05-15"&gt;Hermes Agent Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/" rel="noopener noreferrer"&gt;Hermes Agent official docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;Hermes Agent GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/tools/" rel="noopener noreferrer"&gt;Hermes tools and toolsets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/memory" rel="noopener noreferrer"&gt;Hermes persistent memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/skills/" rel="noopener noreferrer"&gt;Hermes skills system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/messaging" rel="noopener noreferrer"&gt;Hermes messaging gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/security" rel="noopener noreferrer"&gt;Hermes security model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/tools" rel="noopener noreferrer"&gt;OpenClaw tools and plugins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/skills" rel="noopener noreferrer"&gt;OpenClaw skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/architecture" rel="noopener noreferrer"&gt;OpenClaw Gateway architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/security" rel="noopener noreferrer"&gt;OpenClaw security guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do you think?&lt;/p&gt;

&lt;p&gt;Is Hermes actually the next step after OpenClaw, or is OpenClaw still the better model for personal agents?&lt;/p&gt;

&lt;p&gt;And of the five claims above, which one matters most to you: memory, skills, sandboxing, messaging, or running the agent on real infrastructure?&lt;/p&gt;

</description>
      <category>hermesagentchallenge</category>
      <category>devchallenge</category>
      <category>agents</category>
      <category>discuss</category>
    </item>
    <item>
      <title>My GitHub Graveyard has 27 dead projects. Here is the brutal truth about why.</title>
      <dc:creator>S M Tahosin</dc:creator>
      <pubDate>Wed, 13 May 2026 18:32:33 +0000</pubDate>
      <link>https://dev.to/tahosin/my-github-graveyard-has-27-dead-projects-here-is-the-brutal-truth-about-why-52d9</link>
      <guid>https://dev.to/tahosin/my-github-graveyard-has-27-dead-projects-here-is-the-brutal-truth-about-why-52d9</guid>
      <description>&lt;p&gt;I recently opened my GitHub account and filtered by private repositories. I actually counted them: exactly 27 abandoned side projects created over the last 3 years.&lt;/p&gt;

&lt;p&gt;There was a machine-learning habit tracker. There was a Twitter clone for dogs. There was a complex SaaS boilerplate that I spent four weeks configuring before completely giving up on it. Some of them I spent weeks on. One I even bought a domain for.&lt;/p&gt;

&lt;p&gt;Hundreds of hours wasted. Why did they all die before seeing the light of day? It was not a lack of time. It was not a lack of motivation. &lt;/p&gt;

&lt;p&gt;Here is the controversial truth: &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most developers do not fail because of a lack of skill. They fail because they secretly enjoy the dopamine rush of &lt;em&gt;starting&lt;/em&gt; a new project more than the grind of &lt;em&gt;finishing&lt;/em&gt; it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the exact pattern that killed my 27 projects, and the rule that finally helped me break the cycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Perfect Stack" Trap
&lt;/h3&gt;

&lt;p&gt;As developers, we love shiny new tools. When starting a project, the first instinct is to try that new database everyone is talking about on Twitter, or the latest beta version of a framework.&lt;/p&gt;

&lt;p&gt;I once spent an entire weekend configuring a Next.js app with tRPC, Prisma, and a custom Tailwind design system. By Sunday night, my infrastructure was absolute perfection. But I had zero business logic written. The next day, I lost interest completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want to actually finish a project, you have to use boring technology.&lt;/strong&gt; Pick the stack you know best, even if it feels outdated.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Optimizing for Phantom Users
&lt;/h3&gt;

&lt;p&gt;For the dog Twitter clone, I spent three days setting up a complex Redis caching layer. I was terrified the server would crash if a million dogs signed up on day one. &lt;/p&gt;

&lt;p&gt;We love to over-engineer. We worry about how our database will handle massive traffic, so we design complex microservices. But here is the brutal reality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your biggest threat is not the server crashing. Your biggest threat is that nobody will ever visit your site.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stop building for problems you do not have yet. A simple database query is fine. You can always optimize later when the app actually gets traction.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Feature Creep is a Disease
&lt;/h3&gt;

&lt;p&gt;It starts innocently. You are building a simple to-do list, and you think, "It would be cool if users could upload a custom profile picture." Suddenly, you are reading AWS S3 documentation for five hours instead of finishing the core task logic.&lt;/p&gt;

&lt;p&gt;Features are fun to dream about, but they are heavy to build. &lt;strong&gt;Every extra button you add delays the launch.&lt;/strong&gt; The best way to finish a project is to ruthlessly cut features until you have the absolute minimum viable product. If it does not solve the core problem, it gets deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Fear of Shipping
&lt;/h3&gt;

&lt;p&gt;Writing code is safe. Your VS Code editor does not judge you. But launching a project means real people might see it, find bugs, or worse—ignore it completely. &lt;/p&gt;

&lt;p&gt;A lot of side projects are abandoned right at the 90 percent mark because the developer is secretly afraid of hitting the deploy button. We hide behind the excuse of "it just needs a little more polish." &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A buggy, ugly app that is live on the internet is infinitely more valuable than a perfect app sitting on localhost.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The 48-Hour Rule
&lt;/h3&gt;

&lt;p&gt;To break this curse, I made a strict new rule for myself: &lt;strong&gt;I have to launch a working, ugly prototype within 48 hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it takes longer than a single weekend to get the core feature live, the scope is too big. This simple mindset shift is one of the biggest reasons I finally started shipping real apps instead of building graveyards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Over to you
&lt;/h3&gt;

&lt;p&gt;I know I am not the only one with a GitHub graveyard of dead ideas. &lt;/p&gt;

&lt;p&gt;Be honest: &lt;strong&gt;What is the weirdest abandoned side project you have ever started, and what was the &lt;em&gt;real&lt;/em&gt; reason you stopped working on it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me know in the comments. What is in your graveyard?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
