<?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: Kiita</title>
    <description>The latest articles on DEV Community by Kiita (@kiita1302).</description>
    <link>https://dev.to/kiita1302</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%2F4124172%2F690a7ca2-4603-4c92-b48d-313df0a5e68f.jpg</url>
      <title>DEV Community: Kiita</title>
      <link>https://dev.to/kiita1302</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiita1302"/>
    <language>en</language>
    <item>
      <title>Six Bugs That Taught Me How to Build an AI Agent That Doesn't Fall Over</title>
      <dc:creator>Kiita</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:49:51 +0000</pubDate>
      <link>https://dev.to/kiita1302/six-bugs-that-taught-me-how-to-build-an-ai-agent-that-doesnt-fall-over-3mpp</link>
      <guid>https://dev.to/kiita1302/six-bugs-that-taught-me-how-to-build-an-ai-agent-that-doesnt-fall-over-3mpp</guid>
      <description>&lt;p&gt;For the past several months I've been running a voice-driven AI assistant of my own design, continuously, unattended — 240+ tools, real API integrations, real background jobs, no human babysitting it around the clock.&lt;/p&gt;

&lt;p&gt;Building the demo version took a weekend. Keeping it alive, unsupervised, for months took a lot longer — and almost none of what I had to fix ever shows up in a framework's quickstart guide. It only shows up after enough real hours logged, with a real user (me) depending on it not breaking silently at 2am.&lt;/p&gt;

&lt;p&gt;Here are three of the failures that taught me the most, and what I ended up building because of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The OAuth helper that hung for hours
&lt;/h2&gt;

&lt;p&gt;One integration used a standard OAuth flow: open a browser, wait for the user to click "allow." That's fine when a human is sitting there. It is not fine when the same code path gets triggered from a scheduled background task with an expired token and nobody anywhere near a screen.&lt;/p&gt;

&lt;p&gt;It just... waited. For hours. No error, no timeout, no log line saying anything was wrong — just a thread quietly blocked forever, because nothing in the code had ever considered "what if no human answers."&lt;/p&gt;

&lt;p&gt;The fix sounds obvious in hindsight: never let anything block indefinitely when it might run unattended. In practice, doing this correctly from &lt;em&gt;any&lt;/em&gt; thread, on both Windows and POSIX, without being able to forcibly kill a stuck call, took real care — Python has no cross-platform signal-based way to interrupt an arbitrary blocking call from a non-main thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Two duplicate tool calls, two live sessions
&lt;/h2&gt;

&lt;p&gt;The model occasionally emits the same tool call twice in a single turn — once on a first pass, once on what looks like a retry. Most of the time this is harmless. Once, it wasn't: two near-simultaneous calls to a "launch session" tool spun up two concurrent audio sessions on the same physical device. Two processes fighting over one microphone is exactly as unpleasant as it sounds.&lt;/p&gt;

&lt;p&gt;The fix is a short time-windowed dedupe check — tool name plus arguments, hashed, checked against what ran in the last few seconds. Simple once you know you need it. I didn't know I needed it until it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. One shared API key, two features starving each other
&lt;/h2&gt;

&lt;p&gt;Two unrelated features shared a single provider API key. When one had a burst of usage, it silently ate into the quota the other feature needed — mid-conversation, with no warning, no isolation, no way to tell which feature was actually responsible for the 429 that showed up somewhere else entirely.&lt;/p&gt;

&lt;p&gt;The fix: every paid call now sits behind a budget ceiling tracked &lt;em&gt;per feature&lt;/em&gt;, not per account. A runaway loop in one capability can no longer starve every other capability sharing the same key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with all of this
&lt;/h2&gt;

&lt;p&gt;Every pattern above — plus three more (code-enforced confirmation gates for irreversible actions, a structured audit trail, and a watchdog that restarts what dies) — is now a small, tested, zero-dependency Python library called &lt;strong&gt;kevlar-agent&lt;/strong&gt;.&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;kevlar-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;kevlar_agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedupe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget_guard&lt;/span&gt;

&lt;span class="nd"&gt;@dedupe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@budget_guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_gen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monthly_limit_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.04&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;generate_clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh_oauth_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;24 tests, MIT licensed, no framework lock-in — it's plain Python decorators and functions, so it drops into whatever you're already building (LangChain, CrewAI, MCP-based agents, or nothing at all) instead of asking you to adopt a new architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/KiitaInternet/kevlar" rel="noopener noreferrer"&gt;https://github.com/KiitaInternet/kevlar&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Full write-up with all six patterns:&lt;/strong&gt; &lt;a href="https://kiitainternet.github.io/kevlar/" rel="noopener noreferrer"&gt;https://kiitainternet.github.io/kevlar/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're running an agent unattended and have hit any of these — or a failure mode I haven't — I'd genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
