<?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>Forem: Tijo Gaucher</title>
    <description>The latest articles on Forem by Tijo Gaucher (@rapidclaw).</description>
    <link>https://forem.com/rapidclaw</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3850323%2F4c57502d-d13a-4255-aa80-30e2ab22d035.jpeg</url>
      <title>Forem: Tijo Gaucher</title>
      <link>https://forem.com/rapidclaw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rapidclaw"/>
    <language>en</language>
    <item>
      <title>[3 Reliability Patterns That Stopped My AI Agent From Crashing Every 6 Hours]</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 18 May 2026 02:13:13 +0000</pubDate>
      <link>https://forem.com/rapidclaw/3-reliability-patterns-that-stopped-my-ai-agent-from-crashing-every-6-hours-49f9</link>
      <guid>https://forem.com/rapidclaw/3-reliability-patterns-that-stopped-my-ai-agent-from-crashing-every-6-hours-49f9</guid>
      <description>&lt;p&gt;I'm running five AI agents in production. None of them are the ambient "does your whole job" kind everyone's demoing on X. They're boring: a research agent that scrapes pricing data overnight, a cold-email agent that drafts and queues replies, a coding agent that triages GitHub issues, a screenshot/QA agent for our marketing pages, and one that just runs scheduled reports.&lt;/p&gt;

&lt;p&gt;For the first month, every one of them died on a six-to-twelve hour cadence. Sometimes a tool call would hang forever. Sometimes the model would return a token the parser choked on. Sometimes it was just OOM. The agent would freeze, the cron-style triggers would queue up behind it, and I'd find out the next morning when nothing had run.&lt;/p&gt;

&lt;p&gt;Three patterns took me from "babysit the process" to "haven't looked at the dashboard in a week." Here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat the agent like any other long-running process: supervise it
&lt;/h2&gt;

&lt;p&gt;The first instinct people have is to put their agent script in a &lt;code&gt;while True:&lt;/code&gt; loop and call it good. Don't. The loop dies with the process — and the process dies more than you think.&lt;/p&gt;

&lt;p&gt;I put every agent under &lt;code&gt;supervisord&lt;/code&gt; (you can use &lt;code&gt;systemd&lt;/code&gt; if you prefer; the point is the same). The config is maybe ten lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[program:research-agent]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python /opt/agents/research.py&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;startretries&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;stderr_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/agents/research.err.log&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/agents/research.out.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things this gets you. First, &lt;strong&gt;restart on crash&lt;/strong&gt; — &lt;code&gt;autorestart=true&lt;/code&gt; brings the process back even when the Python interpreter exits non-zero. Second, &lt;strong&gt;logs that survive the crash&lt;/strong&gt;, because supervisord captures stderr to a file the agent itself never opened. Third, &lt;strong&gt;a single command to see state&lt;/strong&gt; — &lt;code&gt;supervisorctl status&lt;/code&gt; tells you which agents are alive without grepping &lt;code&gt;ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The number that mattered after I did this: my "agent uptime" went from 71% to 99.4% in a week. Nothing about the agent code changed. The whole win was running it like a real service instead of a script.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Persist state outside the process
&lt;/h2&gt;

&lt;p&gt;Restart-on-crash is only useful if the agent doesn't lose its place when it comes back. The default for most agent frameworks is to hold the conversation history, the to-do list, and any cached tool outputs in memory — all of which vanish the moment the process dies.&lt;/p&gt;

&lt;p&gt;Two layers of persistence cover almost every case I've hit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint after every tool call.&lt;/strong&gt; Before the agent loop calls the next tool, write the current state (messages, pending tasks, partial outputs) to disk or SQLite. After a restart, the first thing the agent does is read the checkpoint and resume from the last completed step. The overhead is a few milliseconds per turn — nothing compared to the inference latency you're already paying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a real queue for incoming work.&lt;/strong&gt; If your agent runs on a schedule or responds to webhooks, don't have the trigger call the agent directly. Push the trigger onto a queue (Redis, SQS, or a database table with a &lt;code&gt;claimed_at&lt;/code&gt; column) and have the agent pull from it. When the agent dies mid-task, the queue still has the job, and the next run picks it up.&lt;/p&gt;

&lt;p&gt;These two together mean a crash costs you one redo of the last tool call — not a whole night of missed work.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bound everything with timeouts (and don't trust the SDK defaults)
&lt;/h2&gt;

&lt;p&gt;The single biggest source of "agent is alive but doing nothing" was tool calls that hung. A scraping target stops responding. A &lt;code&gt;subprocess.run&lt;/code&gt; with &lt;code&gt;shell=True&lt;/code&gt; never returns. The model SDK's "default" timeout turns out to be three minutes, and you didn't notice.&lt;/p&gt;

&lt;p&gt;Wrap every tool the agent can call in a timeout. In Python:&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;signal&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="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hard_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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;_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hit &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGALRM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGALRM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then any tool call gets &lt;code&gt;with hard_timeout(30): ...&lt;/code&gt;. Pick the budget based on what the tool actually does — 5s for a curl, 30s for a scrape, 120s for a model call. The point is that &lt;em&gt;something&lt;/em&gt; fires.&lt;/p&gt;

&lt;p&gt;Pair this with a circuit breaker for tools that fail repeatedly. If the same tool times out three times in a row, mark it broken for ten minutes and route the agent to a fallback (or just have it skip and log). The alternative — letting the agent retry indefinitely on a broken endpoint — burns tokens and blocks every other task in the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;After all three patterns: 99.4% uptime across the five agents, average time-to-recovery on a crash dropped from "next morning when I noticed" to under 30 seconds, and token spend on retries dropped by about 40%. The agents got &lt;em&gt;less smart&lt;/em&gt; in some sense — they no longer try heroic recovery — and that's the trade. Boring agents that run forever beat clever agents that occasionally do magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you stop doing this yourself
&lt;/h2&gt;

&lt;p&gt;I built all this on a $20/mo VPS I administer myself. It works. But by month three I noticed I was spending more time on the supervision layer than on the agents themselves — tweaking restart policies, rotating logs, sizing the checkpoint table, debugging why one agent's port forward broke when another one OOMed.&lt;/p&gt;

&lt;p&gt;If you're in that loop and you'd rather not be — building your agent and operating your agent are different jobs — managed hosting takes the operational layer off your plate. &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw's Builder Sandbox&lt;/a&gt; ($99/mo) is the same MicroVM-with-sudo setup I'm describing here, with the supervisor, checkpointing, and timeouts already wired up. The &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;Dev Agent tier&lt;/a&gt; adds snapshot/rollback so a bad deploy doesn't take the whole agent down.&lt;/p&gt;

&lt;p&gt;Either way, the patterns are the patterns. Whether you operate them yourself or let &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;someone else run the agent host&lt;/a&gt;, supervise the process, persist the state, bound the calls. Your agents will outlive you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tijo Gaucher is the founder of &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt;, managed hosting for AI agents.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>[I Ran 5 AI Agents Unattended for 30 Days] What Actually Broke and What Held</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 11 May 2026 02:11:54 +0000</pubDate>
      <link>https://forem.com/rapidclaw/i-ran-5-ai-agents-unattended-for-30-days-what-actually-broke-and-what-held-1ckn</link>
      <guid>https://forem.com/rapidclaw/i-ran-5-ai-agents-unattended-for-30-days-what-actually-broke-and-what-held-1ckn</guid>
      <description>&lt;p&gt;When I tell operators their AI assistant will "just run 24/7," that's the promise. The reality is uglier — agents crash, sessions die, context windows fill up, model providers throttle, and your "automation" becomes a 3AM page.&lt;/p&gt;

&lt;p&gt;Last month I gave myself a constraint: run 5 small agents unattended for 30 days as a solo founder. No babysitting, no manual restarts. One for inbox triage. One for monitoring a few competitor pricing pages. One for nightly browser-based status checks. One for code refactor batch jobs. One for content scraping.&lt;/p&gt;

&lt;p&gt;Here's what actually broke, what held, and the reliability patterns I'd ship before letting a non-technical operator anywhere near an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four failure modes I hit (in order of frequency)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context window bloat → silent degradation.&lt;/strong&gt; This was the most insidious. The agent didn't crash — it just got progressively dumber. By day 4, the inbox agent was misclassifying obvious spam because the conversation history was bumping against the limit and the most recent emails were displacing the routing rules. No exception, no alert. Just bad work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Model provider throttling.&lt;/strong&gt; Day 11. Rate limits I didn't know existed kicked in mid-batch. The agent threw a 429, didn't have a retry path, and silently stopped processing the queue. I found out 6 hours later when the backlog showed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Auth token expiry.&lt;/strong&gt; The scraping agent died on day 19 when a session cookie aged out. Standard problem, completely predictable, completely missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Memory leaks in long-running browser sessions.&lt;/strong&gt; Headless Chrome doesn't love a 30-day uptime. Day 23, OOM. The monitoring agent took the whole VM with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five reliability patterns that would have prevented all of it
&lt;/h2&gt;

&lt;p&gt;These aren't novel — they're the same patterns you'd apply to any unattended workload. The new part is applying them to an LLM-driven workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Context rotation at fixed intervals.&lt;/strong&gt; Don't let conversation history grow unboundedly. Snapshot the state you care about (decisions, rules, persistent memory), drop the rest, start a fresh context. For the inbox agent, every 200 messages = new context with a summary of routing rules pinned at the top. Simple, fixes the silent degradation problem permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Exponential backoff with provider failover.&lt;/strong&gt; When your primary model throttles, fall back to a secondary. OpenRouter makes this trivial — you configure a fallback chain and forget about it. For most tasks, Claude → Haiku → GPT-4o-mini is plenty. The user never notices when the primary 429s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: Health checks an operator can actually read.&lt;/strong&gt; Not Prometheus, not Grafana. A status page that says "Inbox agent: last action 8 minutes ago" or "Pricing monitor: failed at 2:14am, retried 3 times, paged at 2:20am." The operator should be able to glance at it in the morning and know what to act on. If they have to interpret a graph, you've already lost them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 4: Token refresh as a first-class concern.&lt;/strong&gt; Auth tokens have expiries. Bake them into the agent's lifecycle: rotate proactively, never reactively. If your agent runs longer than your shortest token lifetime, you have a bug — even if it hasn't fired yet. Treat it like SSL renewal: scheduled, alerted, automated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 5: Process-level rollback on resource thresholds.&lt;/strong&gt; When memory or CPU breaches a threshold, snapshot the agent state, kill the process, restart from snapshot. This is boring infra work. It's also what makes the difference between "my agent ran for 30 days" and "my agent ran for 4 days, three times in a row."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in a managed world
&lt;/h2&gt;

&lt;p&gt;If you're an operator — not a developer — you don't want to set up any of this. You want your assistant to run, you want to know when it doesn't, and you want a vendor to fix the second case before you notice the first.&lt;/p&gt;

&lt;p&gt;That's what &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;managed agent hosting&lt;/a&gt; is supposed to solve. Not "we run a container for you" (that's just hosting). The actual job is the five patterns above, plus the 50 others I haven't written about yet, applied consistently so the operator never sees them.&lt;/p&gt;

&lt;p&gt;I'm building toward this with &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt; — managed OpenClaw hosting tiered for SMEs. The Builder Sandbox tier ($99/mo, MicroVM with sudo + live port-forwarding) is where agents like the five above live. The Dev Agent tier ($200/mo) adds observability and snapshot/rollback specifically because patterns 3 and 5 above kept biting me during this test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring stuff is the moat
&lt;/h2&gt;

&lt;p&gt;The ambient-agent-does-your-job narrative is still mostly vibes. What's actually working in production today is the boring stuff — scheduled jobs that run reliably, browser automation that doesn't die overnight, coding agents that finish their refactor without losing the plot at hour 4.&lt;/p&gt;

&lt;p&gt;That's not a sexy story. But it's the story that pays. The patterns above aren't novel; they're table stakes for any unattended workload. The reason most agent stacks don't have them is because most agent stacks are demos that got deployed.&lt;/p&gt;

&lt;p&gt;If you're running agents yourself, the five patterns above are free advice — apply them, you'll have a better month than I did. If you'd rather not think about any of it, that's the &lt;a href="https://rapidclaw.dev/pricing" rel="noopener noreferrer"&gt;pitch for managed agent hosting&lt;/a&gt;: the boring stuff, handled, so you can run 5 agents at once and not wake up at 3AM.&lt;/p&gt;

&lt;p&gt;Either way: don't ship an agent into production without context rotation, failover, health checks, token refresh, and resource thresholds. The next 30-day uptime story you tell will be better for it.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>What I found auditing my own homepage for AI Overview compatibility</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Sun, 10 May 2026 09:21:41 +0000</pubDate>
      <link>https://forem.com/rapidclaw/what-i-found-auditing-my-own-homepage-for-ai-overview-compatibility-5gb7</link>
      <guid>https://forem.com/rapidclaw/what-i-found-auditing-my-own-homepage-for-ai-overview-compatibility-5gb7</guid>
      <description>&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%2Fzilk72mli0gmrpqa97sk.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%2Fzilk72mli0gmrpqa97sk.png" alt="SERP mockup with rapidclaw.dev not cited" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have been quietly losing top-of-funnel traffic to a thing I cannot click on. Not a competitor. Not a Reddit thread. The Google AI Overview box at the top of the SERP. The thing that pulls a paragraph out of the open web, paraphrases it, slaps a few citation chips next to it, and answers the question before the user ever scrolls down to my listing. For a year I have been telling myself I would do something about it. Last week I finally sat down and audited my own homepage to figure out why I was not getting cited.&lt;/p&gt;

&lt;p&gt;The audit took most of a Saturday. Some of it was infuriating. Some of it was satisfying in the way that only finding and fixing a stupid mistake can be. I want to write down what I actually found, because most of the AEO and "answer engine optimization" writing I have read this year is too abstract to act on. It is full of words like "entity-level relevance" and "knowledge graph alignment" and not enough screenshots of the JSON your homepage is missing.&lt;/p&gt;

&lt;p&gt;I run the content side of a small two-founder hosted-OpenClaw shop here in Bali. My brother Brandon runs the infra. Neither of us is a SEO consultant. I have a content background and I read a lot, but most of the GEO and AEO advice in 2026 is being written by people selling a course about it, and I do not trust the course people. So this is the working operator's version. Here is what was wrong with my homepage and what I changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing I did was query my own product
&lt;/h2&gt;

&lt;p&gt;This sounds dumb but I had not actually done it. I opened a clean browser window, signed out of everything, and asked Google "what is the cheapest managed openclaw hosting" the way a buyer would. Then I asked "is rapidclaw any good" and "rapidclaw vs the alternatives". I watched the AI Overview render each time and I read what it said.&lt;/p&gt;

&lt;p&gt;The Overview talked about my competitors. It did not mention me. The citation chips next to the answer were two competitors and a Hacker News comment thread. My homepage was nowhere. The organic listing for rapidclaw.dev was sitting at position three on the page underneath the Overview, which used to be a fine place to be in 2022. In 2026 it is a place where almost nobody reads.&lt;/p&gt;

&lt;p&gt;The interesting part was that the Overview answer was wrong about my competitors in a small way. It was confidently citing a price tier that one of them had retired six months ago. So this was not a quality bar problem. The Overview was happy to repeat outdated stuff. The bar was something simpler. My page just was not in the consideration set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second thing I did was read my own HTML
&lt;/h2&gt;

&lt;p&gt;I right-clicked my homepage and asked for the page source. Then I pasted it into a structured-data validator. This is the kind of thing I should have been doing once a quarter for the last three years. I had not been doing it. The result was embarrassing.&lt;/p&gt;

&lt;p&gt;My homepage had no organization-level schema. None. The og:image meta tag was pointing at a screenshot that we deprecated last spring and the URL was a 404. The h1 read something marketing-coded that did not contain the actual product category. The meta description was the placeholder Vercel had auto-generated when we redeployed in February and it said "rapidclaw.dev — built with Next.js" because we had never overwritten it.&lt;/p&gt;

&lt;p&gt;It was rough. I am writing this down in detail because I think more founders are running pages like this than admit it. The marketing-site code path tends to be the one that drifts the most, because it is the one nobody is paying explicit attention to. Brandon owns infra. I own content. The marketing site is the seam between us and the seam is where rust grows.&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%2Fdepxhjprwn2daro5hxfu.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%2Fdepxhjprwn2daro5hxfu.png" alt="The JSON-LD I added" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes I shipped, in the order I did them
&lt;/h2&gt;

&lt;p&gt;I worked top-down through the page source.&lt;/p&gt;

&lt;p&gt;The first thing I added was an &lt;code&gt;Organization&lt;/code&gt; schema block in JSON-LD with the company name, the URL, the logo, and a sameAs array pointing at the X account, the YouTube channel, the GitHub org, and the Indie Hackers profile. None of that information was in machine-readable form anywhere on the homepage. I had been assuming the AI Overview would figure out we were a real entity from context. It does not figure that out. It expects you to declare it.&lt;/p&gt;

&lt;p&gt;The second thing I added was a &lt;code&gt;Product&lt;/code&gt; schema block describing the hosted OpenClaw service, with a price field on the cheapest tier, an &lt;code&gt;aggregateRating&lt;/code&gt; placeholder I will fill in next month when we have enough first-party reviews, and a real &lt;code&gt;description&lt;/code&gt; field that uses the words a buyer would search for. I rewrote the description three times before I was happy. The version I shipped reads less like marketing and more like the answer to a question.&lt;/p&gt;

&lt;p&gt;The third thing I did was rewrite the h1. The old h1 was clever. The new h1 is descriptive. Cleverness is a luxury you can afford when the search interface is humans reading a list of ten blue links. It is not a luxury you can afford when the search interface is a model summarizing the open web in three sentences. I want to be in those three sentences. The model is going to grab whatever in my markup most clearly explains what category I am in. So my h1 now explains what category I am in. There is a separate clever line underneath it for the human who has scrolled. Both audiences are served. Neither is ignored.&lt;/p&gt;

&lt;p&gt;The fourth thing I did was fix the meta description. I wrote a real one. It is forty-eight words and it tells you exactly what we sell, who we sell it to, and what the cheapest option costs. The old one had been the auto-generated placeholder for nearly three months. I want to crawl into a hole when I think about how much referrer traffic I cost myself with that one mistake.&lt;/p&gt;

&lt;p&gt;The fifth thing I did was write an FAQ block with &lt;code&gt;FAQPage&lt;/code&gt; schema. Five questions, real answers, no padding. The questions were the ones the support inbox actually receives every week. "Do I need an API key?" "Is there a free tier?" "What happens if my container goes to sleep?" "Can I bring my own model?" "How do I cancel?" The answers are short enough that an AI Overview can quote them verbatim if it wants to.&lt;/p&gt;

&lt;p&gt;The sixth thing was the og:image. I generated a new social card, hosted it on a stable URL, and updated the meta tag. I also added Twitter card tags, which I had never done because I had been told nobody used Twitter cards anymore in 2024. They are still useful in 2026 for any model that ingests social-media-flavored metadata, which is a thing models do.&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%2Fvq6zba8kfjygaizdsocr.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%2Fvq6zba8kfjygaizdsocr.png" alt="Audit findings table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmarks check is still my favorite part of this work
&lt;/h2&gt;

&lt;p&gt;The reason I care about being cited in AI Overviews specifically is that the buyer who arrives via that path tends to be more qualified than the buyer who arrives via a long-tail organic click. They have already had their question half-answered. They are clicking through because they want the rest of the answer or because they want to verify the citation. They are not browsing. They are auditing.&lt;/p&gt;

&lt;p&gt;If you are in the AI agent infrastructure category, the buyer is also auditing performance numbers. There is a whole class of buyer who will not click a hosted-agent product page until they have checked the same product against a public benchmark. That is the buyer I have been writing for over the past two months. The deep-dive I keep getting search traffic on is &lt;a href="https://rapidclaw.dev/blog/agentbench-leaderboard-2026" rel="noopener noreferrer"&gt;the AgentBench 2026 leaderboard rundown&lt;/a&gt; — it goes through the top results, which prompts gamed which categories, and what the numbers actually mean for an operator picking a hosting layer. That piece does not need to mention rapidclaw fifteen times to convert. It just needs to be the most useful version of itself. The conversion takes care of itself when the reader trusts that you are reading the same data they are.&lt;/p&gt;

&lt;p&gt;I bring this up because it is the same lesson as the homepage audit. The AI Overview is going to cite the page that most clearly answers the question. It is not going to cite the page that most aggressively sells you a product. So the work, on both the homepage and on the long-form posts, is to be more useful and less promotional. The promotion is the link at the bottom that the qualified reader clicks because they have already decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not figured out yet
&lt;/h2&gt;

&lt;p&gt;A few open questions I am working on.&lt;/p&gt;

&lt;p&gt;I do not know whether the JSON-LD changes are going to show up in the AI Overview within days, weeks, or months. I have read every plausible answer to this on the web and there is no consensus. I will know in a few weeks whether my homepage starts getting cited. If it does, I will write the follow-up. If it does not, I will write the failure post-mortem.&lt;/p&gt;

&lt;p&gt;I do not know if &lt;code&gt;aggregateRating&lt;/code&gt; placeholders without real reviews are worth shipping. The structured data validator does not love it. I am going to leave it out for now and add it once we have enough first-party testimonials to back the number up. I would rather be invisible than be visibly fake.&lt;/p&gt;

&lt;p&gt;I do not know how much of this is going to matter in twelve months. The retrieval layer behind AI Overviews keeps shifting. Maybe by next year the model is good enough to extract organization metadata from natural language without needing the schema block. I do not think we are there yet. I am writing for the version of the model that exists right now. If the model gets smarter, my schema block does no harm. If it does not get smarter, I am suddenly readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell another founder reading this
&lt;/h2&gt;

&lt;p&gt;Three quick ones.&lt;/p&gt;

&lt;p&gt;First, query your own product the way a buyer would, signed out, in a clean browser. Do this once a month. Watch the AI Overview render. If it does not mention you, you have a problem upstream of any clever marketing copy. You have a problem in your HTML.&lt;/p&gt;

&lt;p&gt;Second, the JSON-LD work is small and unglamorous and almost everybody is skipping it. Schema is the cheapest leverage you have. An hour of work fixes a year of invisibility. I am annoyed at myself for waiting.&lt;/p&gt;

&lt;p&gt;Third, the content piece and the homepage piece are the same piece. The model that reads your homepage is the same model that reads your blog posts. The tone you take in your most useful blog post is the tone you should take on your homepage. If your homepage is louder than your blog posts, your homepage is wrong.&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%2F4o0o1yly1hqg8yt87zuf.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%2F4o0o1yly1hqg8yt87zuf.png" alt="Three takeaways" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Brandon is going to push the schema changes through Vercel today. I will check the AI Overview again in two weeks. Either it will start mentioning us, in which case there is a follow-up to write, or it will not, in which case there is a different follow-up to write. Both posts get written. The outcome decides which one.&lt;/p&gt;

&lt;p&gt;— Tijo&lt;/p&gt;

</description>
      <category>ai</category>
      <category>google</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The CTA disclosure test: I added "$29/m after 5 messages" to every Free Trial button. Here's what I expected to happen.</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Sun, 10 May 2026 04:19:46 +0000</pubDate>
      <link>https://forem.com/rapidclaw/the-cta-disclosure-test-i-added-29m-after-5-messages-to-every-free-trial-button-heres-what-i-4776</link>
      <guid>https://forem.com/rapidclaw/the-cta-disclosure-test-i-added-29m-after-5-messages-to-every-free-trial-button-heres-what-i-4776</guid>
      <description>&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%2Fkb1gm4ta0jivddtgslmy.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%2Fkb1gm4ta0jivddtgslmy.png" alt="CTA before/after" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Earlier this week I made a change I had been putting off for two months. I went through every "Start Free Trial" button on the site and rewrote it to read "Start Free Trial — $29/m after 5 messages". Same button, same color, same destination. Just a price tag glued onto the label.&lt;/p&gt;

&lt;p&gt;Then I sat back and waited for conversion to crater.&lt;/p&gt;

&lt;p&gt;That's what I expected. That is what most CRO people I trust have told me to expect over the years. Disclosing a price near the CTA is supposed to scare buyers off. The conventional move is to keep the trial language clean, get the email, and let the upgrade screen handle the money conversation. I have done it that way for a long time. I run the content side of our little two-founder shop here in Bali. My brother Brandon ships the infra. I write the words and run the experiments. So when I changed every CTA on the site, I was running an experiment on my own copy and I was pretty sure the result was going to make me feel stupid.&lt;/p&gt;

&lt;p&gt;It did not make me feel stupid. It made me feel like I had been quietly lying to myself for a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made the change at all
&lt;/h2&gt;

&lt;p&gt;The reason I changed the buttons in the first place had nothing to do with conversion theory. It was a support ticket.&lt;/p&gt;

&lt;p&gt;A user wrote in last Tuesday and said something like: "I clicked Start Free Trial on the homepage and now I am being charged $29 a month and I never saw a price anywhere." He was not angry. He was confused. He thought he was on a free tier. He had clicked through onboarding too quickly, like everybody does, and the trial expiry email landed in his Promotions tab. By the time the charge hit his card he had no memory of what the price even was. The product was working. The bill was correct. The experience was terrible.&lt;/p&gt;

&lt;p&gt;Brandon and I talked about it on a call. Brandon's view was that this was a billing UX problem and he could fix it on the dashboard side. My view was that it was upstream of that. The user did not get blindsided in the dashboard. He got blindsided on the marketing site, where I had spent twelve months carefully not telling him the price near the button he was clicking.&lt;/p&gt;

&lt;p&gt;There is a phrase I keep coming back to when I write copy for the SME audience we care about: a point-of-sale system never surprises you. That is the standard I want for our hosted OpenClaw thing. People should know what they are buying when they click. Even if the click is for a free trial. Especially if the click is for a free trial.&lt;/p&gt;

&lt;p&gt;So I changed the button. Then I changed it everywhere. Hero, pricing page, footer, the comparison tables, the in-line "Try it free" links inside blog posts. Wherever a user could begin the trial flow, the price for what they were beginning was now sitting right next to the button.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected vs. what happened
&lt;/h2&gt;

&lt;p&gt;The hypothesis I went in with was the textbook one. Higher friction at CTA means lower click rate. Lower click rate means fewer trial signups. Fewer trial signups means fewer paid conversions even if intent quality is higher, because most funnels lose to volume.&lt;/p&gt;

&lt;p&gt;The chart in my head looked like a small dip in clicks, an unclear story for trials, and a possibly-positive but possibly-flat story for paid. I told myself this was the kind of thing where you accept a 10-15% conversion hit because the integrity story is worth it. I was bracing for a number that was going to make me defensive on a call with Brandon.&lt;/p&gt;

&lt;p&gt;What actually showed up over four days was that click-through on the changed buttons did not drop in any meaningful way. The wobble I saw was within the noise floor of a site that does not get a billion visits a day. Trial signups looked the same. The piece I did not predict was the support volume change. The "I did not realize this was paid" tickets did not just go down. They stopped. Zero in the four days after the change. The week before the change there had been three.&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%2Flygdbqcy7kxdr1yyntp0.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%2Flygdbqcy7kxdr1yyntp0.png" alt="Support tickets dropped to zero" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to be careful here. Four days is not a study. I do not have enough volume to call this statistically significant. What I am willing to claim is the directional story. The dip I was bracing for did not happen, and the failure mode I was actually paying for did happen to disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think the conventional wisdom missed this one
&lt;/h2&gt;

&lt;p&gt;The CRO playbooks I learned from are almost all built on consumer e-commerce data. A surprise price at the cart on a t-shirt site really does kill conversion, because the buyer has no model for what a t-shirt should cost and the surprise reads as a trick. The price-near-CTA test runs against a buyer who is already loss-averse and already suspicious.&lt;/p&gt;

&lt;p&gt;Our buyer is not that buyer. The person clicking "Start Free Trial" on rapidclaw.dev is usually a small business operator who has been quietly evaluating three or four options. He has been told by somebody on a Reddit thread that AI agents cost money and break overnight. He has read the comparison articles. He has a budget in his head before he gets to my page. When my CTA hides the price, I am not being polite. I am being suspicious. I am giving him a reason to think the real number is higher than the budget he had in mind, otherwise why would I be hiding it?&lt;/p&gt;

&lt;p&gt;This is the lesson I keep relearning when I write for SMEs instead of for developers. SME operators are not afraid of $29 a month. Most of them are paying $29 a month for fifteen different things they barely use. What they are afraid of is the shape of a trick. The trick-shape is "free trial → unexpected charge". The honest-shape is "free trial, $29/m after". Once they see the honest-shape on the button, the rest of the page reads as honest too. And the honest page outperforms the polished one.&lt;/p&gt;

&lt;p&gt;There is a thread connecting this to the content side of what I do. Every time I write a piece that tries to figure out the real number of something — what an AI agent actually costs you to run for a month, what the cheap tier really gets you, where the bills come from — that piece tends to outperform the piece that talks about features. The audience is starving for somebody to do the math out loud. If you are wrestling with this same problem on the infra side, my &lt;a href="https://rapidclaw.dev/blog/openclaw-hosting-cost-self-host-vs-managed" rel="noopener noreferrer"&gt;OpenClaw hosting cost breakdown&lt;/a&gt; goes through the self-host vs. managed numbers in the same flat way I am writing here. I keep coming back to it because the buyers who read that piece convert at a higher rate than buyers who read the hero copy. They have already done the spreadsheet, in their heads, with me.&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%2Fh70k7md1o155912suo4q.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%2Fh70k7md1o155912suo4q.png" alt="Four-day snapshot table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not change, and why
&lt;/h2&gt;

&lt;p&gt;I did not change the upgrade screen. The upgrade screen still reads the same way it has read for a year. I did not change the email sequence. I did not change the dashboard banner. The only change was the marketing site CTAs.&lt;/p&gt;

&lt;p&gt;The reason matters. If I had changed everything at once, I would not be able to say anything about the CTA disclosure specifically. I would have a bundle of changes and a bundle of outcomes and a story I could tell either direction. By keeping the change narrow, I have a thin slice of evidence that the disclosure itself was at worst a wash and possibly a small win. That is enough for me to leave it on. It is not enough for me to write a Twitter thread saying "always disclose price near your CTA, you cowards". I am not writing that thread. I am writing this post.&lt;/p&gt;

&lt;p&gt;The other thing I did not do is run a real A/B test. I do not have the volume on this site for a clean A/B in four days. I changed the buttons globally, watched the metrics I cared about, and decided on the basis of the things that did not happen — clicks did not drop, signups did not drop, support tickets did drop. I am calling that good enough for a button copy decision that took fifteen minutes to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The piece I did not see coming
&lt;/h2&gt;

&lt;p&gt;A thing I noticed, a week in, that I did not predict at all. Trial-to-paid conversion looks like it is creeping up. I am not ready to claim a number on it because the cohort is small and the sample period is short. But it is moving in the right direction and I think I know why. The trial users who came in after the button change are showing up in their first session knowing what they are paying for and when. That sounds obvious, right up until you watch how trial flows usually behave. Most trials get killed by the second-week confusion: the user is not sure if they are using the product correctly, is not sure when the bill comes, has not internalized what the bill even is. A user who saw "$29/m after 5 messages" on the way in does not have that confusion. He sat down at the dashboard with a price already loaded into his head. He is not surprised by anything. He treats the trial like a paid product on day one. So he uses it like a paid product on day one. So he keeps it.&lt;/p&gt;

&lt;p&gt;That is the part I want to write a longer follow-up on. Not yet. I want another two weeks of data first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell another founder reading this
&lt;/h2&gt;

&lt;p&gt;Three things, none of them clever.&lt;/p&gt;

&lt;p&gt;First, the CTA-near-price prohibition is a holdover from a different audience. If you are selling to small business operators with budget in their heads, the conventional move is the wrong move. Price near the button reads as honesty. Hiding the price reads as a trick. The buyer's instinct is not the e-commerce buyer's instinct.&lt;/p&gt;

&lt;p&gt;Second, the metric you should look at is not the click rate on the button. It is the support ticket queue and the trial-to-paid rate. The button is a top-of-funnel thing. The win, if there is one, lands later in the funnel. If you only watch the click rate you will miss the entire story.&lt;/p&gt;

&lt;p&gt;Third, do the change before you can prove the change. I sat on this for two months because I wanted a clean experimental design and I did not have the volume for one. The actual decision took fifteen minutes once I let go of the test-design fantasy. Sometimes you ship the obvious thing and the world tells you what happened.&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%2Flpp5usa2fj1ixfupndk5.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%2Flpp5usa2fj1ixfupndk5.png" alt="Three takeaways" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am leaving the buttons how they are. If the trial-to-paid number keeps moving in this direction, I will write that follow-up. If it drifts back to the old number, I will write that one too.&lt;/p&gt;

&lt;p&gt;Either way I am not putting the price back behind the button. Brandon and I do not run this thing to be clever. We run it to be a place a small business can park its agent and forget about it. Forgetting starts at the button.&lt;/p&gt;

&lt;p&gt;— Tijo&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
      <category>marketing</category>
    </item>
    <item>
      <title>5 ways your AI agent runtime silently dies overnight (and the boring fix for each)</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 04 May 2026 07:40:16 +0000</pubDate>
      <link>https://forem.com/rapidclaw/5-ways-your-ai-agent-runtime-silently-dies-overnight-and-the-boring-fix-for-each-279o</link>
      <guid>https://forem.com/rapidclaw/5-ways-your-ai-agent-runtime-silently-dies-overnight-and-the-boring-fix-for-each-279o</guid>
      <description>&lt;p&gt;I ran the same agent for thirty straight days. It died five times. Four of them did not show up in any log I had set up ahead of time, which is the part that bothers me most.&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%2Foj2gmx8vojzgkaoqif6g.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%2Foj2gmx8vojzgkaoqif6g.png" alt="5 ways your AI agent runtime silently dies overnight" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the end I had a checklist of things that take an agent down at 2am while you're asleep, and none of them are the dramatic failures that get blog posts. They are all dull.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  1. OOM during a long tool-call loop
&lt;/h2&gt;

&lt;p&gt;The agent is happily looping through 200 tool calls in one task. Each call returns a response. The agent appends every response to its working context plus an internal trace it writes to disk. Around call 150, RAM usage starts going up faster than usage going down. By call 180, the kernel OOM-killer wakes up and ends the process.&lt;/p&gt;

&lt;p&gt;In the log: nothing. The agent's stdout cuts off mid-sentence. The supervisor logs say "process exited 137" which is the OOM signal but very few people read it that way the first time.&lt;/p&gt;

&lt;p&gt;The boring fix: cgroup memory limits with a soft warning at 80%, plus a tool-call counter that flushes the working trace to disk every 25 calls and resets the in-memory copy. Not exotic. Just remembering that long agent loops are basically a memory leak unless you actively flush.&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%2Fxoa5gezkm9fzvykl1l09.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%2Fxoa5gezkm9fzvykl1l09.png" alt="30-day run timeline showing five failure points" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. File descriptor exhaustion
&lt;/h2&gt;

&lt;p&gt;Day eleven. The agent had been making API calls all day. A new tool call started and immediately got &lt;code&gt;OSError: too many open files&lt;/code&gt;. The agent caught the exception, tried to retry, got the same error, gave up, returned an error to the user.&lt;/p&gt;

&lt;p&gt;The agent itself didn't crash. It just stopped being useful. The supervisor process saw "agent returned an error" and moved on. Nothing alerted.&lt;/p&gt;

&lt;p&gt;What actually happened: the agent's HTTP client was reusing a session pool that didn't close idle sockets, and over 11 days it had accumulated about 950 open FDs against the per-process default of 1024. Every new HTTP call added to the pool. Eventually it ran out.&lt;/p&gt;

&lt;p&gt;The boring fix: explicit session lifecycle with a timeout, a daily restart of the agent process, and &lt;code&gt;ulimit -n&lt;/code&gt; raised to something sane (16384 on the runtimes I cared about). The daily restart is the cheap one. People resist it because it feels primitive, but every long-running daemon I have ever shipped survives on a daily restart somewhere in the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Context window bloat
&lt;/h2&gt;

&lt;p&gt;This one I had read about, but it still got me. The agent's working context grew to about 180,000 tokens by hour 60 of a multi-day task. Each new tool call cost more than the last because the model was paying to re-read the entire history. By hour 65 a single tool call was taking 90 seconds and burning through the per-minute rate limit, which the agent interpreted as "the API is down" and went into a backoff loop.&lt;/p&gt;

&lt;p&gt;The agent didn't crash. It just got slower and slower until it was producing nothing, and the bill kept going up.&lt;/p&gt;

&lt;p&gt;The boring fix: a context summarizer that runs every N tool calls, replaces the last K turns with a one-paragraph summary, and keeps the most recent 5 turns verbatim. This is well-trodden ground in the literature, but the surprising part is how rarely small teams actually wire it up. Most agent codebases I have looked at assume the conversation will end in a few turns. Long-running agents need garbage collection on their own conversation history.&lt;/p&gt;

&lt;p&gt;If you want a longer treatment of why &lt;a href="https://rapidclaw.dev/blog/ai-agent-hosting-complete-guide" rel="noopener noreferrer"&gt;AI agent hosting&lt;/a&gt; is mostly about boring problems like this one rather than the model itself, the longer version is worth a skim. The summary: the model is the easy part now. Everything around it is where the failures live.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ulimit walls (max user processes)
&lt;/h2&gt;

&lt;p&gt;Day nineteen. The agent had spawned a background process for a long-running task and then gone on to do other work. Background tasks accumulated. By midnight there were 287 zombie processes attached to the agent's user, the per-user &lt;code&gt;max user processes&lt;/code&gt; limit was somewhere around 1024 in this environment, and at 03:14 a new spawn failed with &lt;code&gt;Resource temporarily unavailable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the log: a single line saying the spawn failed. The agent caught it as a generic exception and continued. The user-facing behavior was "this task takes forever." Three days later when I finally noticed I had to manually reap the zombies.&lt;/p&gt;

&lt;p&gt;The boring fix: a process supervisor that owns the lifecycle of every spawned task, kills anything that has been alive longer than its declared TTL, and treats child processes as a resource that needs to be tracked. &lt;code&gt;setsid&lt;/code&gt; and &lt;code&gt;prctl(PR_SET_PDEATHSIG)&lt;/code&gt; are your friends. Also raise &lt;code&gt;ulimit -u&lt;/code&gt; to something generous, but the real fix is killing things on schedule.&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%2Fzh941nmmqwhk5sd05qwq.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%2Fzh941nmmqwhk5sd05qwq.png" alt="Failure mode to fix matrix" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Webhook timeouts that look like success
&lt;/h2&gt;

&lt;p&gt;Last one, and the meanest. The agent finished a task and called a webhook to notify a downstream system. The webhook took 31 seconds to respond. The HTTP client had a 30 second timeout. The client raised a timeout error. The agent's wrapper caught the timeout and &lt;em&gt;logged a success&lt;/em&gt; because the wrapper had been written assuming "timeout means delivered, the receiver was just slow."&lt;/p&gt;

&lt;p&gt;This is true for some kinds of fire-and-forget delivery. It is catastrophic for any kind of state-changing call. The downstream system never received the call. The agent thought it had. The user-facing system had two views of the world that did not agree.&lt;/p&gt;

&lt;p&gt;In the log: a success line. No error. Nothing wrong.&lt;/p&gt;

&lt;p&gt;The boring fix: idempotency keys on every state-changing webhook, a status check after every call that crossed the timeout threshold, and never treat a timeout as success without a separate confirmation. A timeout tells you the status is unknown. It does not tell you the call was delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern across all five
&lt;/h2&gt;

&lt;p&gt;Every one of these failures had the same shape: a long-running agent ran into a resource limit or a state assumption that was fine for short tasks and broken for multi-day ones. The agent itself did not crash in three of the five cases. It just stopped being useful, and the supervisor was not watching for that.&lt;/p&gt;

&lt;p&gt;The hosting layer needs to do three things that aren't sexy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Memory and FD limits with warnings before the hard cap, not at the hard cap&lt;/li&gt;
&lt;li&gt;Process lineage tracking so spawned tasks can't outlive their parent's intention&lt;/li&gt;
&lt;li&gt;State-changing call confirmation, not just transport-level success&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are running an agent on your laptop for an hour, none of this matters. If you are hosting OpenClaw agents in production for paying customers, all of this matters more than the model you picked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I now log on day one of any agent project
&lt;/h2&gt;

&lt;p&gt;The thing that would have saved me the most pain on this run is just better logging from the start. None of the metrics below are exotic. None of them require an APM vendor. They are just the things I now scrape from any agent process before letting it run for more than 24 hours.&lt;/p&gt;

&lt;p&gt;Per agent loop I track: RSS memory, FD count, child process count, total tool calls in this loop, total context tokens, time since last tool call returned, and the result of the last 10 tool calls (success or specific error). Per host I track: free memory, total FDs in use, load average, and the pid count for the agent's user. Both go to a flat file with a timestamp. No dashboard. Just a thing I can grep when something is weird.&lt;/p&gt;

&lt;p&gt;Five of those metrics would have caught four of the five failures I described, hours or days before they actually broke things. The fifth (the webhook timeout) needs application-level logging, not host-level. That one is on the developer of the wrapper.&lt;/p&gt;

&lt;p&gt;I have a longer guide on the hosting end of this at &lt;a href="https://rapidclaw.dev/blog/ai-agent-hosting-complete-guide" rel="noopener noreferrer"&gt;https://rapidclaw.dev/blog/ai-agent-hosting-complete-guide&lt;/a&gt;, but if you read nothing else, read this: the most expensive failure mode is the one that doesn't crash the process. Crashes get noticed. Slow-degrading agents do not.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>llms</category>
    </item>
    <item>
      <title>MicroVM vs Docker for AI agents: I gave one sudo and broke the other</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 04 May 2026 07:38:06 +0000</pubDate>
      <link>https://forem.com/rapidclaw/microvm-vs-docker-for-ai-agents-i-gave-one-sudo-and-broke-the-other-2loc</link>
      <guid>https://forem.com/rapidclaw/microvm-vs-docker-for-ai-agents-i-gave-one-sudo-and-broke-the-other-2loc</guid>
      <description>&lt;p&gt;Last week I ran a small experiment that I should have run a year ago.&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%2Fg1709tyoqyiznvncwuza.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%2Fg1709tyoqyiznvncwuza.png" alt="MicroVM vs Docker for AI agents — cover" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same agent code. Same model. Same task list: install three Python packages from a CSV, fetch a few APIs, write JSON to disk, run a long-running scheduled job. Two isolation modes. One was a Docker container with the agent process inside, mounted volume, the usual. The other was a Firecracker microVM running a slim Linux image with the agent on top. Both got &lt;code&gt;sudo&lt;/code&gt; inside their sandbox. I let them run for seven days each, then rotated.&lt;/p&gt;

&lt;p&gt;I went in expecting the difference to be small. Memory overhead, maybe boot time. The actual difference was bigger than that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day one and two: Docker
&lt;/h2&gt;

&lt;p&gt;Setup was the part everyone has done before. &lt;code&gt;docker run&lt;/code&gt; with a few mounts, the agent gets a shell inside, away we go. The agent is told to &lt;code&gt;apt-get install&lt;/code&gt; a couple of system libraries it has decided it needs. That works. It writes a 40 MB cache file to &lt;code&gt;/tmp&lt;/code&gt;. That works. It runs a long job that opens a few hundred sockets to a public API.&lt;/p&gt;

&lt;p&gt;Around hour eighteen the host machine's &lt;code&gt;dmesg&lt;/code&gt; started printing about memory pressure. Not from the agent itself. From a &lt;em&gt;different&lt;/em&gt; container running on the same host. The Python process inside my agent's container had a retry loop that would not stop holding file descriptors. That was the leak. Linux does not look at which container a process lives in when it picks something to OOM-kill. It just picks. The neighbor went down.&lt;/p&gt;

&lt;p&gt;This is the part of Docker that no production person likes to talk about. Containers share the host kernel. They share the host scheduler. When one container goes off the rails, the rest of them feel it on the same host. If you're a small shop running one agent on one host, fine. None of this matters yet. For anything that looks like a tenant model, it stops being fine fast.&lt;/p&gt;

&lt;p&gt;The other thing I noticed on day two: the agent decided to &lt;code&gt;chmod 777&lt;/code&gt; a folder it didn't own. Not malicious. Just a Python script doing what Python scripts do when permissions throw an error. With &lt;code&gt;sudo&lt;/code&gt; available inside the container, it succeeded. The host filesystem was untouched (because mounted volumes have their own boundary), but anything &lt;em&gt;inside&lt;/em&gt; that container was now wide open to whatever the agent did next.&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%2Fit03r7lm9di81js4q96t.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%2Fit03r7lm9di81js4q96t.png" alt="Isolation layers — Docker vs MicroVM stacks" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Day three: rebuild as a MicroVM
&lt;/h2&gt;

&lt;p&gt;I tore down the Docker setup and rebuilt the same agent inside a Firecracker microVM. Same code, same packages, same task list. Boot time went from about 200 ms (Docker) to about 700 ms (microVM). Memory baseline went up by roughly 60 MB for the kernel itself.&lt;/p&gt;

&lt;p&gt;That is the cost. You pay it once.&lt;/p&gt;

&lt;p&gt;What you get is a separate kernel. The agent's &lt;code&gt;sudo&lt;/code&gt; is a real Linux &lt;code&gt;sudo&lt;/code&gt; inside a real kernel that nobody else on the host shares. When the agent ran the same &lt;code&gt;chmod 777&lt;/code&gt; thing, it still happened, but the blast radius was a single VM that I could destroy and recreate in under a second. When the agent leaked file descriptors, only the VM's per-process limits got hit. The host kernel didn't notice.&lt;/p&gt;

&lt;p&gt;Day four I let the agent install a kernel module on purpose. In Docker this would be a host-level event (or blocked outright, depending on capabilities). In the microVM it loaded into the VM's own kernel and stayed there. Whether that is useful or annoying is up to you. Either way, it stayed inside the VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day five through seven: chaos
&lt;/h2&gt;

&lt;p&gt;I wrote a script that told the agent to do five terrible things in a row, the kind of thing a real long-running agent might trip into over a week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open 10,000 file descriptors and never close them&lt;/li&gt;
&lt;li&gt;Allocate memory in a tight loop until something breaks&lt;/li&gt;
&lt;li&gt;Spawn 200 child processes&lt;/li&gt;
&lt;li&gt;Write a 5 GB file to its own filesystem&lt;/li&gt;
&lt;li&gt;Make a &lt;code&gt;sudo iptables&lt;/code&gt; rule that drops all incoming packets&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%2Fc095nzkclixgcwdr3x5v.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%2Fc095nzkclixgcwdr3x5v.png" alt="Chaos test results — five awful things, two sandboxes" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker handled (1) and (2) with cgroup limits if you remember to set them. Most people don't. The default is no limit. By the time I noticed (1) on the third run, the host had hit its global file descriptor ceiling and SSH started refusing connections.&lt;/p&gt;

&lt;p&gt;The microVM handled (1) through (4) inside its own boundary. The kernel inside the VM hit its limits, the VM started thrashing, and then I killed it from outside. The host machine never noticed any of it. (5) was the most interesting. The agent set the iptables rule and locked itself out of network. In Docker the agent had affected the container's network namespace, recoverable but messy. In the microVM the agent had only touched the VM's network stack. I rebooted the VM in 800 ms and was back.&lt;/p&gt;

&lt;p&gt;Snapshots are where the gap really opened. Firecracker can snapshot a running VM to disk and resume it later. I snapshotted the agent mid-task on day six, killed the host, restored the snapshot on a different machine, and the agent picked up its loop one second later without knowing anything had happened. Try that with a Docker container and you will spend the afternoon learning about CRIU and giving up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The link to actually running this in production
&lt;/h2&gt;

&lt;p&gt;Doing this experiment locally is one thing. Running an agent like this for a paying customer, on hardware you have to keep alive for 30+ days at a stretch, is a different problem. The boring infrastructure problem nobody writes about: it isn't the isolation primitive that's hard. Anyone can spin up Firecracker. The hard part is babysitting a hundred of these things at once, snapshotting them every so often, recovering them when a host dies, and not losing the agent's state in the meantime.&lt;/p&gt;

&lt;p&gt;I'll plug the thing I work on once and move on. The &lt;a href="https://rapidclaw.dev/blog/openclaw-hosting-cost-self-host-vs-managed" rel="noopener noreferrer"&gt;Builder Sandbox tier&lt;/a&gt; is a managed wrapper around exactly this microVM-with-sudo model, with the snapshot and recovery loop already wired up. If you don't want to babysit it, that's the option. If you want to babysit it yourself, Firecracker is open source and the docs are fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;p&gt;Running one tiny agent on a host you own? Docker is fine. The overhead is real, the boundary is real enough for that scope, and you already know the tools.&lt;/p&gt;

&lt;p&gt;The moment you have an agent that needs &lt;code&gt;sudo&lt;/code&gt;, runs for more than a few days, and might do something weird at 3am, switch to a real VM. The 60 MB and the 500 ms of extra boot time will pay for themselves the first time the agent does something stupid. The snapshot story alone is worth the migration.&lt;/p&gt;

&lt;p&gt;The thing I didn't expect, going in, was how much my mental model changed. With Docker I treat the container as a thing the agent lives &lt;em&gt;in&lt;/em&gt;. With a microVM I treat the VM as a thing the agent &lt;em&gt;is&lt;/em&gt;. That shift, more than any individual feature, is what made the seven-day test feel different on day three than it did on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few specifics, in case you try this
&lt;/h2&gt;

&lt;p&gt;The microVM rebuild was Firecracker 1.5 with a vanilla Ubuntu 22.04 rootfs, 2 vCPU, 1 GB RAM, virtio-net for the network. Boot times stayed under a second consistently once I trimmed the kernel config. I used &lt;code&gt;jailer&lt;/code&gt; to drop privileges on the Firecracker process itself, and seccomp filters on the agent's user inside the VM. None of that is exotic. The Firecracker docs cover all of it. The only thing I had to figure out the hard way was the snapshot directory layout, which the docs assume you already understand.&lt;/p&gt;

&lt;p&gt;For Docker the comparison build was the standard &lt;code&gt;python:3.12-slim&lt;/code&gt; base with the agent process as the entrypoint, a tmpfs mount for &lt;code&gt;/tmp&lt;/code&gt;, and &lt;code&gt;--cap-drop=ALL&lt;/code&gt; plus only the capabilities the agent actually needed. Even with that, the chmod-777 case still worked inside the container because &lt;code&gt;sudo&lt;/code&gt; plus &lt;code&gt;CAP_FOWNER&lt;/code&gt; is enough for filesystem-mode changes. You can lock this down further with seccomp profiles, but at that point you have built a worse VM with extra steps.&lt;/p&gt;

&lt;p&gt;If you want the longer cost breakdown of running this yourself versus paying someone to keep it alive, I wrote that up here: &lt;a href="https://rapidclaw.dev/blog/openclaw-hosting-cost-self-host-vs-managed" rel="noopener noreferrer"&gt;https://rapidclaw.dev/blog/openclaw-hosting-cost-self-host-vs-managed&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>hosting</category>
    </item>
    <item>
      <title>[The Boring AI Agent Workloads That Actually Pay in 2026]</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 04 May 2026 04:38:04 +0000</pubDate>
      <link>https://forem.com/rapidclaw/the-boring-ai-agent-workloads-that-actually-pay-in-2026-1lal</link>
      <guid>https://forem.com/rapidclaw/the-boring-ai-agent-workloads-that-actually-pay-in-2026-1lal</guid>
      <description>&lt;p&gt;Every other post on my feed is still pitching the "ambient agent that runs your whole job." If you actually run agents in production, you know that story is mostly vibes. The workloads that real people pay for, repeatedly, look almost embarrassingly mundane.&lt;/p&gt;

&lt;p&gt;After a year of running agents for SMEs — accounting firms, e-commerce shops, two solo law practices — here are the four shapes of work that consistently survive the trial-to-paid conversion. None of them require AGI. All of them require an agent that doesn't fall over on day eleven.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scheduled jobs that used to be cron + a human
&lt;/h2&gt;

&lt;p&gt;The unsexy starting point. A cron job kicks off at 6 AM. It logs into a portal, scrapes a number, drops it in a sheet, and Slacks the team if a threshold trips. That used to be a half-day Selenium project plus a $40/mo VPS plus the ongoing maintenance tax of the portal redesigning itself every quarter.&lt;/p&gt;

&lt;p&gt;An agent flips the math. The same job is now a five-line prompt and a browser tool. The portal redesigning itself is the agent's problem now, not yours. The cost question stops being "how much engineering time" and becomes "how reliable is the runtime."&lt;/p&gt;

&lt;p&gt;That second question is the entire moat for managed agent platforms. It's also the reason most of the open-source-only "just spin up your own" pitches fall apart at month two. The agent works fine. The orchestration around it — retries, secret rotation, the headless browser updating, the model deprecating — is what bleeds the operator dry.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Browser automation that was too brittle for RPA
&lt;/h2&gt;

&lt;p&gt;If you've ever priced UiPath or Automation Anywhere for a small business, you know the answer: it's not for them. The licensing is enterprise-shaped and the bot creation requires a specialist. Meanwhile, the actual workflow — log in, click three things, download a CSV, email it — is the kind of thing every five-person operation needs done weekly.&lt;/p&gt;

&lt;p&gt;Agents with a real sandboxed browser tool eat this category. Not because they're smarter than RPA, but because they degrade gracefully. When the "Export" button moves three pixels left, an agent finds it. When the page adds a cookie banner, an agent dismisses it. The thing that used to take a consultant three days to update takes the agent zero.&lt;/p&gt;

&lt;p&gt;The catch is that "real sandboxed browser" is doing a lot of work in that sentence. A Docker container with a headless Chromium is fine for a demo. For production, you want a MicroVM with sudo so the agent can actually install things, persistent file storage so its session survives a restart, and live port forwarding so you can watch it work when something looks off. That's roughly the hardware bill that &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;managed OpenClaw hosting&lt;/a&gt; abstracts away.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Coding agents that don't touch production
&lt;/h2&gt;

&lt;p&gt;This one is the most counterintuitive. The coding agent market that's working isn't replacing engineers — it's replacing the "I'll get to it next sprint" backlog at companies that don't have engineers.&lt;/p&gt;

&lt;p&gt;Real example: a roofing company. Their internal "system" is a Google Sheet, a Calendly, and three Zapiers. They have a list of forty small tweaks they want — a column added here, a webhook there, a conditional email. None of it is hard. All of it is too small for a contractor and too unfamiliar for the owner. An agent with shell access and the patience to iterate clears that backlog in a weekend. The owner doesn't read the code. The owner reads the result.&lt;/p&gt;

&lt;p&gt;The reliability bar here is different from the production code reliability bar. The agent doesn't need to write perfect code. It needs to not silently break the spreadsheet that runs the business. That's an observability problem, not an intelligence problem. Snapshot the state before each change, let the operator roll back, and the whole category gets safer than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The "always-on assistant" that is actually a search index
&lt;/h2&gt;

&lt;p&gt;The mythology of the AI assistant is that it answers anything. The reality of the paying assistant is narrower: it knows your stuff. Your contracts, your meeting notes, your invoices, your support tickets. It can pull a number out of a 200-page master services agreement faster than the human who wrote the agreement.&lt;/p&gt;

&lt;p&gt;These deployments don't fail because the model is dumb. They fail because the data plumbing is broken — stale embeddings, a connector that silently drops half the documents, a permission boundary that leaks one tenant's data into another. None of which is a model problem.&lt;/p&gt;

&lt;p&gt;This is the workload most people quote when they say "we tried AI and it didn't work." What didn't work was the integration. The model is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually distinguishes the survivors
&lt;/h2&gt;

&lt;p&gt;Look at those four. None of them require a frontier model. None of them require "agentic reasoning" past a couple of hops. What they require is a runtime that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't crash, or recovers gracefully when it does&lt;/li&gt;
&lt;li&gt;Has the right tools wired up (browser, shell, file storage, an email sender)&lt;/li&gt;
&lt;li&gt;Surfaces what it's doing well enough that a non-engineer can tell when it's stuck&lt;/li&gt;
&lt;li&gt;Costs predictably — flat monthly is much easier to sell to a small business than per-token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why the managed-hosting framing is starting to take over the SME conversation. The buyer doesn't want to think about API keys, model selection, or which sandbox their agent is running in. They want the POS-system experience: pay a flat fee, the agent works, somebody else is on the hook when it breaks.&lt;/p&gt;

&lt;p&gt;If you're building this for yourself, the &lt;a href="https://rapidclaw.dev/pricing" rel="noopener noreferrer"&gt;Builder Sandbox tier on RapidClaw&lt;/a&gt; gives you the MicroVM with sudo and live port-forwarding without the infra babysitting. If you're past the building phase and need something an operator can actually run unsupervised, that's the &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;white-glove side&lt;/a&gt; of the same platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd skip in 2026
&lt;/h2&gt;

&lt;p&gt;For completeness — the workloads that keep showing up in pitch decks but quietly failing the trial-to-paid test:&lt;/p&gt;

&lt;p&gt;The "AI sales rep" that prospects and closes by itself. The "AI manager" that runs your team's standup. The "autonomous research analyst" that reads ten papers and synthesizes a thesis. These will get there. They are not there yet. If you're trying to make rent this quarter, build the boring one.&lt;/p&gt;

&lt;p&gt;The boring one is what's paying.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tijo Gaucher runs RapidClaw, managed OpenClaw hosting for non-technical operators. Previously, content at Human + AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>[I ran ONE AI agent for 30 days straight — here's what actually broke]</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Thu, 30 Apr 2026 00:46:23 +0000</pubDate>
      <link>https://forem.com/rapidclaw/i-ran-one-ai-agent-for-30-days-straight-heres-what-actually-broke-7df</link>
      <guid>https://forem.com/rapidclaw/i-ran-one-ai-agent-for-30-days-straight-heres-what-actually-broke-7df</guid>
      <description>&lt;p&gt;Most AI agent demos are shaped like a 90-second loop: prompt → tool call → answer. The interesting failures don't show up there. They show up around day 7, when the process you started in a tmux session has eaten 4 GB of RAM, your browser sub-agent is wedged on a captcha you never noticed, and the thing has been retrying the same failed Stripe webhook for 36 hours.&lt;/p&gt;

&lt;p&gt;I ran a single OpenClaw agent on a small VPS for 30 days. It was scoped to one boring job: triage incoming sales emails, draft replies, file them in the right folder, ping Slack on anything weird. The agent ran continuously, scheduled by cron, with persistent state in SQLite. No multi-agent orchestration, no fancy memory layer — just one process trying to stay alive.&lt;/p&gt;

&lt;p&gt;Here is what actually broke, in the order it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 1–3: everything looks great
&lt;/h2&gt;

&lt;p&gt;The first three days are a honeymoon. Latency is good, the agent handles edge cases I didn't think to specify, and the inbox triage rules quietly improve as it picks up patterns. This is where most demo videos end. It's also where most teams declare victory and move on, which is the mistake.&lt;/p&gt;

&lt;p&gt;Two things to instrument before day 4 even starts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Per-run token cost, written to a flat log. You'll need this when you investigate cost drift in week two.&lt;/li&gt;
&lt;li&gt;Process RSS memory, sampled every minute. The number that matters isn't the peak — it's the slope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're using a hosted setup like &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw's managed OpenClaw runtime&lt;/a&gt;, the slope is graphed for you. If you're self-hosting, write the sampler yourself before you forget. You will forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 4: the context bloat starts
&lt;/h2&gt;

&lt;p&gt;The agent's working memory file grew to 18,000 tokens. None of it was strictly wrong. It was just… accumulated. Old email threads it had handled, notes about edge cases, a half-finished plan for a problem that resolved itself two days earlier.&lt;/p&gt;

&lt;p&gt;The cost per run had quietly tripled.&lt;/p&gt;

&lt;p&gt;This is the most boring failure mode in long-running agents and the one nobody warns you about. Your prompt isn't getting worse — your context window is getting fatter. The fix is unglamorous: a compaction step that runs nightly, summarizes anything older than 48 hours into a few bullet points, and archives the rest to a file the agent can grep but doesn't auto-load.&lt;/p&gt;

&lt;p&gt;If you skip this, by day 14 you're paying GPT-4-class prices to send the model a partially-decayed copy of last week's todo list every single run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 7: the first silent kill
&lt;/h2&gt;

&lt;p&gt;The OOM killer took the process at 3:47 AM. There was no error in the logs because the process didn't get to write one. It just stopped existing.&lt;/p&gt;

&lt;p&gt;This is where most self-hosted agent setups quietly die in production and the operator doesn't notice for two days. The cron job that runs the agent every 15 minutes also exits cleanly when the process is gone — there's no parent supervising health.&lt;/p&gt;

&lt;p&gt;Three things you want before day 7:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A liveness file the agent touches on every successful run, plus an external check that alerts when it's stale for more than 30 minutes.&lt;/li&gt;
&lt;li&gt;A systemd unit (or equivalent) with &lt;code&gt;Restart=on-failure&lt;/code&gt; and &lt;code&gt;MemoryMax=&lt;/code&gt; set well below your VPS's actual RAM. You want the agent to die predictably and come back, not get reaped silently.&lt;/li&gt;
&lt;li&gt;Logs that flush on every event, not on buffer fill. A buffered log is a log you don't have when the OOM killer arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also the point where the "managed hosting" pitch starts to make economic sense for non-developers. Setting up systemd, a watchdog, log shipping, and metric scraping for &lt;em&gt;one&lt;/em&gt; agent is two evenings of work for a competent backend engineer. SMEs don't have that engineer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 11: the captcha trap
&lt;/h2&gt;

&lt;p&gt;The agent's browser sub-task hit a captcha while loading a vendor portal. It didn't fail. It didn't error. It just waited. For 90 minutes. Then the headless Chrome process leaked and the next 14 runs spawned new Chrome instances on top of it.&lt;/p&gt;

&lt;p&gt;The lesson is that anything involving a real browser needs both a hard wall-clock timeout and a "did the page actually finish loading the thing I asked for?" assertion. A 200 response is not a success signal when the body is a captcha challenge.&lt;/p&gt;

&lt;p&gt;If your agent does any web automation at all, this will happen to you. The honest version of the agent demo isn't "watch it browse the web" — it's "watch the watchdog kill a stuck browser session and surface a human-readable reason for it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 18: model drift on the provider side
&lt;/h2&gt;

&lt;p&gt;The replies started getting weirdly formal. Not wrong — just off. I couldn't reproduce it on Claude with the same prompt locally, but in production the change was clear over a 3-day window.&lt;/p&gt;

&lt;p&gt;Eventually I figured out the provider had silently routed a percentage of traffic to a slightly different model variant. This is a real thing that happens, and the only way you catch it is logging a stable hash of the prompt and the full response for every run, then diffing aggregates week-over-week. If you're not doing this, you'll just notice "vibes feel different" and have no evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 24: the small bug that hid in the schedule
&lt;/h2&gt;

&lt;p&gt;A timezone bug in the cron expression meant the agent ran exactly zero times for 18 hours during a holiday DST shift. Nobody noticed because there was no one in the inbox to notice. The triage queue piled up, and the agent's first run after the gap took 11 minutes and 92,000 tokens to dig out.&lt;/p&gt;

&lt;p&gt;Schedules are infrastructure. Test them on a fake clock before you ship them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 30: what stuck
&lt;/h2&gt;

&lt;p&gt;The agent is still running. The job is unglamorous, the per-run cost is now lower than day 1 because of the compaction step, and most weeks I don't think about it. That's the real success criterion for a long-running agent: do you stop having to think about it?&lt;/p&gt;

&lt;p&gt;The narrative around "ambient AI agents that do your whole job" is still mostly vibes. The agents that actually pay rent today are boring: scheduled jobs, browser automation, coding agents, inbox triage. They're sticky because once you have one running and supervised, the cost of replacing it is high. They're hard because the supervision is the actual product.&lt;/p&gt;

&lt;p&gt;If you're a developer building these for yourself, lean into systemd, structured logs, and a 5-line health check. If you're not — or you're shipping this for non-technical operators who can't be on-call for a Python process — managed runtimes like &lt;a href="https://rapidclaw.dev/pricing" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt; exist precisely because day-7 reliability is a product, not a feature.&lt;/p&gt;

&lt;p&gt;The demo is easy. The 30-day uptime is the moat.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tijo writes about practical AI agents at &lt;a href="https://humanai.news" rel="noopener noreferrer"&gt;Human + AI&lt;/a&gt;. RapidClaw is the managed-hosting side of the same operator-focused practice — built for people who want a working AI assistant without becoming a Linux admin.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>observability</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[The 8-Turn Problem] Why Your Agent Fails at Turn 3 and You Only Notice at Turn 7</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 20 Apr 2026 04:32:26 +0000</pubDate>
      <link>https://forem.com/rapidclaw/the-8-turn-problem-why-your-agent-fails-at-turn-3-and-you-only-notice-at-turn-7-534c</link>
      <guid>https://forem.com/rapidclaw/the-8-turn-problem-why-your-agent-fails-at-turn-3-and-you-only-notice-at-turn-7-534c</guid>
      <description>&lt;p&gt;Last Tuesday an agent I shipped decided, mid-conversation, that the user's name was "Export CSV." It wasn't. Seven turns earlier, a tool result had come back with a quoted header row where a &lt;code&gt;username&lt;/code&gt; field should have been, and the model silently absorbed that string as ground truth. Every subsequent turn degraded quietly — apologetic tone, subtle hallucinations, a refusal that referenced "your account, Export CSV."&lt;/p&gt;

&lt;p&gt;The per-call logs looked fine. The latencies were green. Token usage was nominal. The only way to see the break was to reconstruct the whole conversation as a causal graph and follow the poison forward.&lt;/p&gt;

&lt;p&gt;This is the 8-turn problem. It's the single most expensive class of bug I ship, and most of the observability stacks I've tried were built for a world where requests are independent. They aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why request-level monitoring lies
&lt;/h2&gt;

&lt;p&gt;Traditional APM assumes a request is a closed unit: it came in, it did something, it came out, and if you aggregate p99 and error rate you know whether the system is healthy. That model was fine for stateless services. It's openly broken for agents.&lt;/p&gt;

&lt;p&gt;An agent request carries state that isn't in the HTTP payload. It carries the conversation. It carries the tool results that previous turns wrote into context. It carries the model's own prior outputs, which are now training the next inference. A turn that looks locally correct — valid JSON, successful tool call, reasonable response — can be the exact moment your agent quietly goes off the rails for the next 40 minutes of user conversation.&lt;/p&gt;

&lt;p&gt;I watch three numbers more than I watch latency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn-over-turn intent drift&lt;/strong&gt;: does turn N still match the user's original ask?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool result contamination rate&lt;/strong&gt;: how often does a tool response contain strings that look like instructions?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session success rate&lt;/strong&gt;, not request success rate: did the user actually get what they came for?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those are visible from a metrics dashboard that aggregates individual calls. You need traces that span the whole session, and you need them structured so you can walk them backward from the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a useful trace actually looks like
&lt;/h2&gt;

&lt;p&gt;The OpenTelemetry GenAI SIG has been converging on &lt;code&gt;gen_ai.*&lt;/code&gt; semantic conventions, which is good. The prevailing shape: each tool call, each LLM invocation, each retrieval is a child span, parented to the turn, parented to the session. Do that, and your trace tree tells the story of the reasoning chain.&lt;/p&gt;

&lt;p&gt;A few things people get wrong here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't put prompts in span attributes.&lt;/strong&gt; Attributes are indexed, have size caps, and leak straight into your observability backend as PII. Use span events. Events can be sampled, redacted, or dropped at the Collector without touching app code. This one change will save you a compliance conversation later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parent spans by the turn, not just the call.&lt;/strong&gt; If every LLM call is a root span, you lose the conversational structure. The parent-child relationship between turn 3 and turn 7 is the thing you actually want to trace. If you're building this yourself, each session gets a &lt;code&gt;trace_id&lt;/code&gt;, each turn gets a &lt;code&gt;span_id&lt;/code&gt; under it, and tool calls and inferences nest under the turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emit a "decision" span.&lt;/strong&gt; The LLM call itself is one span, but what the agent &lt;em&gt;did&lt;/em&gt; with the output — picked a tool, rephrased, escalated — is a different concern and worth its own span. This is where drift shows up first.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt; we default to this layout and bolt on session-level rollups so you can ask "which turn did this fail at?" without scrolling through 40 spans.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debugging workflow that actually works
&lt;/h2&gt;

&lt;p&gt;When a user reports an agent did something weird, the temptation is to grep logs for the error. There's usually no error. Here's the loop I run instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pull the full session trace.&lt;/strong&gt; Not the failing turn — the whole conversation, from the first user message forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff the system state between turns.&lt;/strong&gt; What changed in memory, in the scratchpad, in the retrieved context? This is where you find the poisoned field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay from the suspected turn with the same tool responses.&lt;/strong&gt; Most agent frameworks let you rehydrate a session; if yours doesn't, you need to fix that before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutate one variable at a time.&lt;/strong&gt; Change the tool response. Change the model. Change the system prompt. Bisect until the behavior flips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the regression test at the session level.&lt;/strong&gt; Not a unit test on a single call — a full conversation fixture with expected final state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where most teams stall. If you can't replay a session deterministically, you're guessing. The &lt;a href="https://rapidclaw.dev/features" rel="noopener noreferrer"&gt;replay and re-simulate workflow&lt;/a&gt; is the single feature I'd build first in any agent observability tool, including ones I don't run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical hygiene for small teams
&lt;/h2&gt;

&lt;p&gt;I run a small operation — think five agents in production, not five hundred — and the infrastructure choices reflect that. A few things that have held up at this scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One OTLP pipeline, everything flows through it.&lt;/strong&gt; Don't run a separate tracing stack for agents. Emit &lt;code&gt;gen_ai.*&lt;/code&gt; spans into the same Collector your regular services use, then branch at the exporter if you want a specialized backend for LLM-specific analysis. Vendor lock-in is a real risk and OTel is the escape hatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample aggressively on success, keep everything on failure.&lt;/strong&gt; Full-conversation traces are expensive. A 1% tail-based sampler plus 100% retention for sessions that flagged any of: tool error, user thumbs-down, abnormal turn count, or model refusal — that gives you the signal without drowning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag sessions with the outcome, not just the request.&lt;/strong&gt; Instrument your app to send a session-end event with "did the user get what they wanted?" If you can't answer that, instrument it first. Every other metric is downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat evals and tracing as the same system.&lt;/strong&gt; Evaluation runs are just traces with known expected outputs. The moment you split them into different tools you start writing glue code that never gets maintained.&lt;/p&gt;

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

&lt;p&gt;Most agent reliability issues I've seen in the last six months aren't model issues. They're context management issues. The model is doing its job — taking what's in the window and producing a plausible next token. The bug is upstream, in what we let accumulate in that window.&lt;/p&gt;

&lt;p&gt;Observability for agents is, practically, observability for the context window over time. If your tooling can't show you how a single field mutated across seven turns, it can't help you debug the 8-turn problem. And the 8-turn problem is most of the bugs.&lt;/p&gt;

&lt;p&gt;If you want to see how we handle session-level tracing in practice, the &lt;a href="https://rapidclaw.dev/docs" rel="noopener noreferrer"&gt;RapidClaw quickstart&lt;/a&gt; walks through instrumenting a LangGraph agent in about ten minutes. But the principle matters more than the tool: trace the session, not the request, and save yourself the compliance conversation by keeping prompts out of attributes.&lt;/p&gt;

&lt;p&gt;Your agents are going to hallucinate. The question is whether you find out at turn 3 or turn 73.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>observability</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementing A2A Protocol for Multi-Agent Communication</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Sat, 18 Apr 2026 03:42:08 +0000</pubDate>
      <link>https://forem.com/rapidclaw/implementing-a2a-protocol-for-multi-agent-communication-2mah</link>
      <guid>https://forem.com/rapidclaw/implementing-a2a-protocol-for-multi-agent-communication-2mah</guid>
      <description>&lt;p&gt;If you've ever wired two AI agents together, you know the drill. Custom JSON schemas, bespoke HTTP endpoints, and a growing pile of adapter code that nobody wants to maintain. Google's A2A (Agent-to-Agent) protocol is the answer to that mess, and I've been implementing it across OpenClaw and Hermes agents on &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;Rapid Claw&lt;/a&gt; for the past few weeks. Here's what the implementation actually looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What A2A solves (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;A2A standardizes the message envelope between independent agents. Think of it as the TCP/IP of agent communication — it defines how agents discover each other, exchange structured messages, delegate tasks, and return results. It doesn't care what framework you're using internally.&lt;/p&gt;

&lt;p&gt;The key distinction: MCP (Model Context Protocol) handles agent-to-tool communication. A2A handles agent-to-agent communication. You need both in any serious multi-agent deployment, and they compose cleanly because an A2A peer is essentially a tool with an agent on the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The envelope format
&lt;/h2&gt;

&lt;p&gt;Every A2A message carries the same required fields. The interesting bits go in &lt;code&gt;payload&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;envelope&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;a2a_version&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;1.0&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;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;msg_&lt;/span&gt;&lt;span class="si"&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="si"&gt;}&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;correlation_id&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;conv_01HZKXR7...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# ties the conversation together
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&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;4bf92f3577b34da6...&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;span_id&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;00f067aa0ba902b7&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sender&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&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;planner-openclaw-prod-01&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;framework&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;openclaw&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;recipient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&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;executor-hermes-prod-03&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;framework&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;hermes&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;intent&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;task.delegate&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;payload&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&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;summarize_and_file&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;inputs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&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;https://example.com/report.pdf&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;constraints&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deadline_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&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;reply_to&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;https://agents.rapidclaw.dev/a2a/planner/inbox&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;expires_at&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;2026-04-18T12:34:56Z&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;Three fields do the heavy lifting: &lt;code&gt;correlation_id&lt;/code&gt; threads multi-agent conversations into a single trace, &lt;code&gt;trace&lt;/code&gt; carries OpenTelemetry-compatible span context so your existing APM stitches everything together, and &lt;code&gt;intent&lt;/code&gt; is the verb recipients dispatch on — not a URL path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing an OpenClaw agent as an A2A endpoint
&lt;/h2&gt;

&lt;p&gt;An OpenClaw agent becomes an A2A peer by exposing an inbox and registering with a platform registry. The agent doesn't need to know who will call it — only how to respond:&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openclaw&lt;/span&gt; &lt;span class="kn"&gt;import&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;Task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a2a&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;planner&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="nf"&gt;from_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;planner.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/a2a/inbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Envelope&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TRUSTED_SIGNERS&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature verification failed&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;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task.delegate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result.return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sender&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;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AGENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;framework&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;openclaw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;payload&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;status&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;ok&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;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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&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="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller discovers executors by label, not URL — this is the part A2A gets right. No hardcoded hostnames:&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;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task.execute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;labels&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;framework&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;hermes&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;env&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;prod&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;h2&gt;
  
  
  Three patterns worth implementing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Request/reply&lt;/strong&gt; is the simplest. Planner calls executor, waits for the reply envelope, acts on it. Use for sub-tasks with clear deadlines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fan-out/fan-in&lt;/strong&gt; dispatches the same intent to a pool of executors in parallel, correlates replies by &lt;code&gt;correlation_id&lt;/code&gt;, and takes the first good answer or aggregates. This is how you build research-agent ensembles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async with callback&lt;/strong&gt; fires a &lt;code&gt;task.delegate&lt;/code&gt; with a &lt;code&gt;reply_to&lt;/code&gt; URL and returns immediately. The callee POSTs a &lt;code&gt;result.return&lt;/code&gt; when done. You get durability without holding an HTTP connection open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The platform layer matters
&lt;/h2&gt;

&lt;p&gt;The protocol is the easy part. Production A2A needs five things at the platform layer: a registry for discovery, identity and mTLS per agent, routing with network policy, observability that stitches traces across agents, and per-agent rate limits. You can build all five yourself — Postgres registry, Vault for keys, Envoy for mTLS, OTEL collector, Redis for rate limits — or use something like &lt;a href="https://rapidclaw.dev/blog/a2a-protocol-ai-agent-hosting" rel="noopener noreferrer"&gt;Rapid Claw&lt;/a&gt; that ships them preconfigured.&lt;/p&gt;

&lt;p&gt;If you're thinking about multi-agent architectures more broadly, I wrote up the common &lt;a href="https://rapidclaw.dev/blog/multi-agent-orchestration-patterns" rel="noopener noreferrer"&gt;orchestration patterns&lt;/a&gt; (planner/executor, supervisor, blackboard) that pair well with A2A as the transport layer.&lt;/p&gt;

&lt;p&gt;A2A isn't revolutionary — it's the boring infrastructure piece that was missing. And boring infrastructure is exactly what you want when you're trying to ship agent systems that actually work in production.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>[Patterns] AI Agent Error Handling That Actually Works</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Fri, 17 Apr 2026 08:47:16 +0000</pubDate>
      <link>https://forem.com/rapidclaw/patterns-ai-agent-error-handling-that-actually-works-1a57</link>
      <guid>https://forem.com/rapidclaw/patterns-ai-agent-error-handling-that-actually-works-1a57</guid>
      <description>&lt;p&gt;Most AI agent tutorials show the happy path. Your agent calls an LLM, gets a response, does the thing. Ship it.&lt;/p&gt;

&lt;p&gt;Then production happens. Rate limits. Timeouts. Malformed responses. Context window overflows. Your agent goes from "demo-ready" to "incident-generating" in about 48 hours.&lt;/p&gt;

&lt;p&gt;I run a small operation — 5 agents max, solo founder. Every failure that wakes me up at 3am is one I should have handled in code. Here are the patterns that actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify Your Errors First
&lt;/h2&gt;

&lt;p&gt;Not all errors deserve the same treatment. The first thing I do in any agent system is classify failures into two buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transient errors&lt;/strong&gt;: Rate limits (429), timeouts, temporary network blips, model overload. These will probably work if you try again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent errors&lt;/strong&gt;: Invalid API keys, malformed prompts, context window exceeded, model doesn't exist. Retrying won't help.&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;ErrorClassifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;TRANSIENT_CODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status_code&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ErrorClassifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANSIENT_CODES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transient&lt;/span&gt;&lt;span class="sh"&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This classification drives everything downstream. Transient errors get retries. Permanent errors get logged, reported, and gracefully degraded. When you're thinking about &lt;a href="https://rapidclaw.dev/blog/ai-agent-security-best-practices" rel="noopener noreferrer"&gt;agent security patterns&lt;/a&gt;, error classification also matters — permanent auth errors need different alerting than transient network hiccups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry Strategies That Don't Make Things Worse
&lt;/h2&gt;

&lt;p&gt;The naive approach — retry immediately, retry forever — is how you turn a rate limit into a ban. Exponential backoff with jitter is the baseline:&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;retry_with_backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&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;attempt&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="n"&gt;max_retries&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;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&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;e&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;ErrorClassifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;  &lt;span class="c1"&gt;# Don't retry permanent errors
&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&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="k"&gt;raise&lt;/span&gt;

            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;delay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key details: jitter prevents thundering herd when multiple agents hit the same limit. And always cap your retries — 3 is usually enough. If it hasn't worked in 3 tries, it's not going to work in 30.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers for LLM Calls
&lt;/h2&gt;

&lt;p&gt;Retries handle individual failures. Circuit breakers handle systemic ones. If your LLM provider is having a bad day, you don't want every request queuing up and timing out.&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;CircuitBreaker&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;failure_threshold&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="n"&gt;recovery_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;failure_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;failure_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failure_threshold&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;recovery_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recovery_time&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;last_failure_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# closed = normal, open = blocking
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&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;fn&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&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;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="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;last_failure_time&lt;/span&gt; &lt;span class="o"&gt;&amp;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;recovery_time&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;half-open&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;CircuitOpenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Circuit breaker is open&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;half-open&lt;/span&gt;&lt;span class="sh"&gt;"&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&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;failure_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&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;e&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;failure_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_failure_time&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="k"&gt;if&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;failure_count&lt;/span&gt; &lt;span class="o"&gt;&amp;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;failure_threshold&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrap every external LLM call in a circuit breaker. When the circuit opens, agents fall back to cached responses or simpler logic instead of piling up failures. If you're taking an &lt;a href="https://rapidclaw.dev/blog/ai-agent-observability" rel="noopener noreferrer"&gt;observability-first approach&lt;/a&gt;, you'll want to track circuit state transitions — they're one of the best early warning signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback Chains: Your Safety Net
&lt;/h2&gt;

&lt;p&gt;When your primary model fails, having a fallback chain prevents total outage:&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;FALLBACK_CHAIN&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&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;anthropic&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;model&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;claude-sonnet-4-20250514&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&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;openai&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;model&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;gpt-4o-mini&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&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;local&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;model&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;cached_response&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;call_with_fallback&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="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FALLBACK_CHAIN&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="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&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;return&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AllProvidersFailedError&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;All &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; providers failed: &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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&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;The chain degrades gracefully: premium model → cheaper model → cached/static response. Your users get &lt;em&gt;something&lt;/em&gt; even when everything is on fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeout Handling
&lt;/h2&gt;

&lt;p&gt;LLM calls are slow. An agent waiting 120 seconds for a response that's never coming is wasting resources and blocking downstream work.&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;asyncio&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&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;30&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&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="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM call exceeded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s limit&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;Set aggressive timeouts. For most agent tasks, if you haven't gotten a response in 30 seconds, something is wrong. I default to 30s for completions and 10s for embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here's how these patterns compose in a real 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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agent_execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;breaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_circuit_breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_calls&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;retry_with_backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;call_with_fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&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="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&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="nc"&gt;AgentResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;CircuitOpenError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AgentResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;degraded&lt;/span&gt;&lt;span class="sh"&gt;"&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;get_cached_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Using cached response - LLM circuit open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AllProvidersFailedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AgentResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All providers unavailable&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 key insight: every layer has a defined failure mode. Timeouts prevent hangs. Retries handle blips. Circuit breakers prevent cascading failures. Fallbacks provide degraded-but-functional responses.&lt;/p&gt;

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

&lt;p&gt;Error handling is only useful if you know it's working. For my small setup, I track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error classification distribution&lt;/strong&gt; — am I seeing more transient or permanent errors?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker state changes&lt;/strong&gt; — how often are circuits opening?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback chain depth&lt;/strong&gt; — how far down the chain are requests going?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry success rate&lt;/strong&gt; — are retries actually recovering errors?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having &lt;a href="https://rapidclaw.dev/features" rel="noopener noreferrer"&gt;real-time error monitoring&lt;/a&gt; changed how I build agents. Instead of finding out about failures from users, I catch patterns before they become outages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boring Truth
&lt;/h2&gt;

&lt;p&gt;None of these patterns are novel. Circuit breakers come from distributed systems. Retry with backoff is older than most of us. Fallback chains are just failover by another name.&lt;/p&gt;

&lt;p&gt;But applying them specifically to AI agents — where failures are probabilistic, responses are non-deterministic, and costs compound with every retry — that's where the craft is. Start with error classification, layer on retries, add circuit breakers, and build fallback chains. Your 3am self will thank you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>errors</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>[2026] OpenTelemetry for LLM Observability — Self-Hosted Setup</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Fri, 17 Apr 2026 08:43:05 +0000</pubDate>
      <link>https://forem.com/rapidclaw/2026-opentelemetry-for-llm-observability-self-hosted-setup-335o</link>
      <guid>https://forem.com/rapidclaw/2026-opentelemetry-for-llm-observability-self-hosted-setup-335o</guid>
      <description>&lt;p&gt;I've been running a small AI automation shop — just me, a handful of agents, and a self-hosted stack that needs to stay observable without blowing the budget. When I started instrumenting my LLM pipelines, I found that most observability guides assumed you'd use a managed platform. But if you're like me and prefer to own your data and infrastructure, OpenTelemetry gives you a solid, vendor-neutral foundation.&lt;/p&gt;

&lt;p&gt;Here's what I've learned getting OpenTelemetry working for LLM agent traces on a self-hosted setup in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenTelemetry for LLM Workloads?
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry (OTel) has become the de facto standard for distributed tracing, metrics, and logs. The ecosystem matured significantly through 2025, and the semantic conventions for generative AI — covering LLM calls, token usage, model parameters — landed as stable in early 2026.&lt;/p&gt;

&lt;p&gt;For LLM workloads specifically, OTel gives you a few things that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace continuity across agent steps.&lt;/strong&gt; When your agent calls an LLM, retrieves from a vector store, then calls another LLM, each step is a span in a single trace. You see the full chain, not just isolated API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token and cost attribution.&lt;/strong&gt; The gen_ai semantic conventions include attributes like &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt; and &lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;, which let you track per-request costs without bolting on a separate billing layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vendor neutrality.&lt;/strong&gt; Whether you're calling OpenAI, Anthropic, or a local model via vLLM, the instrumentation shape is the same. Swap providers without rewriting your observability code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Self-Hosted Stack
&lt;/h2&gt;

&lt;p&gt;My setup is modest — a single VPS running the collection and storage layer, with agents deployed separately. Here's the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Your LLM Agents]
       |
       v
[OTel Collector]  ← receives traces via OTLP/gRPC
       |
       v
[Tempo / Jaeger]  ← trace storage
[Prometheus]      ← metrics storage
[Grafana]         ← visualization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've looked at the &lt;a href="https://rapidclaw.dev/blog/openclaw-hosting-cost-self-host-vs-managed" rel="noopener noreferrer"&gt;self-hosted vs managed cost comparison&lt;/a&gt;, you know the economics are favorable when you're running fewer than five agents. The managed platforms charge per span or per seat, which adds up quickly even at small scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the OTel Collector
&lt;/h2&gt;

&lt;p&gt;The Collector is the central hub. It receives telemetry from your agents, processes it, and exports to your storage backends. Here's a minimal config for LLM traces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# otel-collector-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4318&lt;/span&gt;

&lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;send_batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
  &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment.environment&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upsert&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp/tempo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tempo:4317&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:8889&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp/tempo&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic here. The batch processor keeps things efficient, and we're exporting traces to Tempo and metrics to Prometheus. If you want a deeper walkthrough on getting this into production, the &lt;a href="https://rapidclaw.dev/blog/deploy-openclaw-production-guide" rel="noopener noreferrer"&gt;production deployment guide&lt;/a&gt; covers Docker Compose configs and health checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting LLM Calls
&lt;/h2&gt;

&lt;p&gt;The actual instrumentation depends on your language and SDK. I'll show Python since that's what most agent code runs on.&lt;/p&gt;

&lt;p&gt;First, install the packages:&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;opentelemetry-api opentelemetry-sdk &lt;span class="se"&gt;\&lt;/span&gt;
  opentelemetry-exporter-otlp-proto-grpc &lt;span class="se"&gt;\&lt;/span&gt;
  opentelemetry-instrumentation-requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set up a tracer and wrap your LLM calls:&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;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.exporter.otlp.proto.grpc.trace_exporter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize
&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://your-collector:4317&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;insecure&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm-agent&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;call_llm&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="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-20250514&lt;/span&gt;&lt;span class="sh"&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;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.call&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="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.system&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;anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&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;your_llm_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&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="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&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="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.response.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is using the &lt;code&gt;gen_ai.*&lt;/code&gt; semantic conventions consistently. This means your Grafana dashboards, alerts, and queries work the same regardless of which model or provider you're hitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracing Multi-Step Agent Workflows
&lt;/h2&gt;

&lt;p&gt;Where this gets really useful is tracing a full agent workflow. Each tool call, retrieval step, and LLM invocation becomes a child span:&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;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&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;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.run&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="n"&gt;parent&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;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 1: retrieve context
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieval.vector_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_vector_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 2: call LLM with context
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_llm&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;Context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&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="c1"&gt;# Step 3: maybe call a tool
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;needs_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&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.execute&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="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;tool_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&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.name&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_search&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_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_llm&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;Tool result: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Original task: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you view this in Grafana via Tempo, you get a waterfall trace showing exactly where time was spent — was it the vector search? The first LLM call? The tool execution? This is the kind of visibility that makes debugging agent behavior tractable instead of guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Actually See in the Dashboard
&lt;/h2&gt;

&lt;p&gt;Once everything is wired up, your &lt;a href="https://rapidclaw.dev/features" rel="noopener noreferrer"&gt;self-hosted observability dashboard&lt;/a&gt; shows you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency breakdown per agent step&lt;/strong&gt; — which spans are slow, and whether it's network or model inference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token usage over time&lt;/strong&gt; — catch runaway prompts before they drain your API budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rates by model/provider&lt;/strong&gt; — spot degraded model endpoints early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace search&lt;/strong&gt; — find the exact trace where an agent went off the rails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a solo operator running a few agents, this level of visibility is the difference between confidently shipping agent workflows and crossing your fingers every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rough Edges and Honest Takes
&lt;/h2&gt;

&lt;p&gt;A few things that are still annoying in 2026:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-instrumentation for LLM SDKs is patchy.&lt;/strong&gt; The OpenAI Python SDK has decent OTel support now, but Anthropic's is still experimental. You'll likely write some manual spans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace volume can surprise you.&lt;/strong&gt; Agents that loop — retries, multi-turn conversations — generate a lot of spans. Set up sampling early. A simple tail-based sampler that keeps error traces and samples 10% of success traces works well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grafana dashboards take time to build.&lt;/strong&gt; The gen_ai semantic conventions are new enough that there aren't many pre-built dashboards. Budget an afternoon to set up your panels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry for LLM observability isn't a silver bullet, but it's the most practical foundation I've found for self-hosted setups. The semantic conventions are mature enough to use in production, the Collector is rock-solid, and the cost of running your own Tempo + Grafana stack is a fraction of what you'd pay for a managed platform.&lt;/p&gt;

&lt;p&gt;If you're running a handful of agents and want to actually understand what they're doing, this stack is worth the setup time.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>ai</category>
      <category>observability</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
