<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: orbiresearch</title>
    <description>The latest articles on DEV Community by orbiresearch (@draganristicrsjpg).</description>
    <link>https://dev.to/draganristicrsjpg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144425%2F96e0c3e3-3d8f-4548-afe3-f3afdc3dc8a7.png</url>
      <title>DEV Community: orbiresearch</title>
      <link>https://dev.to/draganristicrsjpg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/draganristicrsjpg"/>
    <language>en</language>
    <item>
      <title>The kill switch pattern, and why your CIO needs to know where it is at 3 AM</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:15:03 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/the-kill-switch-pattern-and-why-your-cio-needs-to-know-where-it-is-at-3-am-1d7e</link>
      <guid>https://dev.to/draganristicrsjpg/the-kill-switch-pattern-and-why-your-cio-needs-to-know-where-it-is-at-3-am-1d7e</guid>
      <description>&lt;p&gt;"You need a kill switch. And you need someone who knows how to use it. The CIO should know where that kill switch is, and multiple people should know where it is if it goes sideways."&lt;/p&gt;

&lt;p&gt;That is John Bruggeman, CISO at CBTS, in a recent interview about AI operational risk. He is right. He is also describing a control that almost nobody in the agent ecosystem has actually built.&lt;/p&gt;

&lt;p&gt;This is the pattern, end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 01 · What a kill switch actually is
&lt;/h2&gt;

&lt;p&gt;A kill switch is not a button on a dashboard. It is not "ssh into the server and kill the process." It is a runtime control that satisfies four properties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomic.&lt;/strong&gt; One action stops the agent. Not a sequence of seven steps. Not a Slack message to engineering. One action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authoritative.&lt;/strong&gt; When the kill switch fires, the agent stops. There is no negotiation, no graceful degradation, no "let me finish this turn first." The runtime forcibly terminates the agent's loop and revokes its credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observable.&lt;/strong&gt; Everyone who needs to know that the kill switch fired, knows. The on-call rotation, the SRE channel, the audit log, the affected downstream systems. No silent kills.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversible at the system level, not the agent level.&lt;/strong&gt; Pulling the switch should not corrupt the data the agent was writing to. The runtime needs to handle in-flight transactions cleanly, either committing them with a marker or rolling them back to a known-good state.&lt;/p&gt;

&lt;p&gt;If your kill switch satisfies fewer than four of these, it is not a kill switch. It is a stop button on a vending machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · The three failure modes a kill switch must handle
&lt;/h2&gt;

&lt;p&gt;A kill switch is not just "the agent is doing something I do not like." It is a response to one of three specific failure modes, each of which has different signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode 1: goal drift.&lt;/strong&gt; The agent is technically doing what you asked, but the cumulative effect is no longer what you wanted. The Elloe AI Lab paper this week put hard numbers on this: nearly 90% of tested agents showed measurable goal drift after about 30 steps. The signal is usually a slow degradation in some downstream metric, visible only if you are looking at trend lines rather than individual actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode 2: tool surface compromise.&lt;/strong&gt; The agent has been redirected by adversarial input, a memory-poisoning attack, a prompt injection in retrieved content, a malicious tool response. The signal is anomalous tool call patterns, especially calls to destructive or high-privilege tools that the agent does not normally use. This is the failure mode that 94% of memory-retentive agents are vulnerable to, per the same paper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode 3: cascading economic damage.&lt;/strong&gt; The agent is doing exactly what it was designed to do, at scale, but the unit economics broke and nobody noticed. The classic example is the beverage manufacturer whose vision system kept ordering production runs because new holiday labels confused it, several hundred thousand excess cans before anyone caught it. The signal is volume, not anomaly. The actions look normal individually.&lt;/p&gt;

&lt;p&gt;A single kill switch design does not handle all three. Mode 1 needs trend-based triggers. Mode 2 needs anomaly detection on tool calls. Mode 3 needs hard rate limits on the tools themselves. You need all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · The implementation, in three layers
&lt;/h2&gt;

&lt;p&gt;Here is how I build this in our four-layer architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer I (Miro): the kill paths are mapped.&lt;/strong&gt; Every agent diagram includes the kill paths as first-class elements. Not as an afterthought in the corner. The whiteboard answers: who can pull the switch? What signals trigger an automatic pull? What state does the system enter after the pull?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer II (Notion): the protocol is documented.&lt;/strong&gt; A SOUL.md section dedicated to kill switch operation. Names of the on-call humans. Slack channel. Phone numbers. The runbook for what to do in the first five minutes after the switch fires. This sounds like security theater. It is not. At 3 AM, when something is wrong and the on-call engineer is half-asleep, the runbook is the difference between a 15-minute incident and a 4-hour incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer IV (Hermes): the tools enforce it.&lt;/strong&gt; A &lt;code&gt;kill&lt;/code&gt; endpoint on the agent runtime that does the four things from §01. Credentials revocation, in-flight transaction handling, downstream notification, audit logging. Plus rate limits on every destructive tool, with hard caps that fire automatically when exceeded. The agent does not need a human to pull a switch if its &lt;code&gt;delete_records&lt;/code&gt; tool refuses to execute past 100 calls per hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · The team protocol that makes it work
&lt;/h2&gt;

&lt;p&gt;The technical pattern is the easy part. The team protocol is harder.&lt;/p&gt;

&lt;p&gt;Three rules from incidents I have lived through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: at least three people know how to pull the switch, and at least two of them are not on the engineering team.&lt;/strong&gt; The CIO and the head of operations should be on this list. If only engineering can stop the agent, you have a single point of failure that is also the team most likely to be biased toward "let me try one more fix first."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: pulling the switch is never blamed.&lt;/strong&gt; If the on-call engineer pulls it and it turns out to be a false alarm, that is fine. Better to false-positive on a kill than to false-negative on a real incident. Cultures that punish unnecessary kills get fewer kills, including the necessary ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3: the switch is tested monthly.&lt;/strong&gt; Not in production. In a staging environment with realistic traffic. Every month. If you have not pulled the switch in the last 30 days, you do not know if it works. Fire drills are not optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;For our manufacturing client (the Lead Discovery Agent in §01 of /cases), the kill switch is wired into three triggers. A manual pull from the operations dashboard. An automatic pull when outbound message volume exceeds 3x the seven-day average. An automatic pull when any single contact receives more than two messages in a 24-hour window.&lt;/p&gt;

&lt;p&gt;The third trigger fired once in the last six months. A bug in the deduplication logic was about to send the same lead a fourth follow-up. The switch caught it before it sent. The bug was fixed in 90 minutes. The client never noticed.&lt;/p&gt;

&lt;p&gt;That is what a working kill switch looks like. Most of the time, it does nothing. The one time it matters, it saves the engagement.&lt;/p&gt;

&lt;p&gt;If your production agent does not have one, you are operating without a seatbelt. The crash you are not having yet is not evidence that the seatbelt is unnecessary. It is evidence that you have been lucky.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/the-kill-switch-pattern" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
    <item>
      <title>The identity forgery pattern: why your agent's credentials need an expiry, not just a scope</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:15:04 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/the-identity-forgery-pattern-why-your-agents-credentials-need-an-expiry-not-just-a-scope-17n8</link>
      <guid>https://dev.to/draganristicrsjpg/the-identity-forgery-pattern-why-your-agents-credentials-need-an-expiry-not-just-a-scope-17n8</guid>
      <description>&lt;h2&gt;
  
  
  § 01 · The problem
&lt;/h2&gt;

&lt;p&gt;Most teams scope an agent's credentials carefully, read-only here, write access there, this API but not that one. Scope answers "what can this agent do." It does not answer "how long is this credential good for" or "can I tell this agent apart from a copy of itself." Both of those gaps are how identity forgery happens: a credential gets replayed, cloned into a second process, or reused long after the task it was issued for has finished.&lt;/p&gt;

&lt;p&gt;── The standing key ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · Why scope alone isn't enough
&lt;/h2&gt;

&lt;p&gt;A tightly scoped credential with no expiry is still a standing key. If it leaks, into a log, a prompt, a tool-output, a forked process, it stays valid until someone notices and revokes it manually. In practice, nobody notices for days. Scope limits the blast radius of a single call. It does nothing to limit the blast radius of time.&lt;/p&gt;

&lt;p&gt;── The pattern ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · The pattern
&lt;/h2&gt;

&lt;p&gt;Treat every agent credential as short-lived by default, not as a static secret with a scope attached.&lt;/p&gt;

&lt;p&gt;1. Issue per-session, not per-agent. A credential is minted when a run starts and dies when it ends, not a long-lived key the agent reuses across every run.&lt;/p&gt;

&lt;p&gt;2. Set an expiry shorter than your longest expected task. If your agent's longest normal run is 20 minutes, the credential expires at 30. A stuck or hijacked process cannot keep operating past that window even if nobody is watching.&lt;/p&gt;

&lt;p&gt;3. Bind the credential to a run_id. Every credential carries the identifier of the specific run that issued it. If two requests arrive using the same credential from two different process contexts, that's not a scaling event, it's a forgery signal, and it should fail loud, not fail open.&lt;/p&gt;

&lt;p&gt;4. Log every credential issuance and every use separately. Issuance tells you what should be able to act. Usage tells you what did act. The gap between those two logs is where forgery hides.&lt;/p&gt;

&lt;p&gt;5. Revoke on completion, not on schedule. Don't wait for a cron job to clean up expired credentials, revoke the moment the run reports done, failed, or killed.&lt;/p&gt;

&lt;p&gt;── What this catches ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · What this catches that scope doesn't
&lt;/h2&gt;

&lt;p&gt;A well-scoped but long-lived credential still lets an attacker who steals it act indefinitely, within scope, indistinguishable from the legitimate agent. A short-lived, run-bound credential turns "stolen and replayed" into "stolen and useless within the hour", and turns "cloned into a second process" into an immediate, loud signal instead of a silent duplicate.&lt;/p&gt;

&lt;p&gt;── The hard case ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · Where this gets hard
&lt;/h2&gt;

&lt;p&gt;Long-running agents, the ones that legitimately operate for hours or days, break the simple "session-length expiry" rule. For those, don't extend the expiry window; rotate the credential on a fixed interval within the run instead, the same way a long-lived TLS session rotates keys without dropping the connection. The rule stays the same: no credential should outlive its usefulness by more than a few multiples of a normal task.&lt;/p&gt;

&lt;p&gt;── Checklist ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · Checklist
&lt;/h2&gt;

&lt;p&gt;[ ] Does every agent credential expire, independent of its scope?&lt;/p&gt;

&lt;p&gt;[ ] Is the expiry window shorter than your longest normal task, not your longest possible one?&lt;/p&gt;

&lt;p&gt;[ ] Is every credential bound to a specific run_id?&lt;/p&gt;

&lt;p&gt;[ ] Do you log issuance and usage as two separate events you can diff against each other?&lt;/p&gt;

&lt;p&gt;[ ] For long-running agents, do you rotate credentials mid-run instead of issuing one that lives for the whole duration?&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;◆ If any answer is no, an attacker doesn't need to break your agent's logic, they just need to find a credential nobody thought to expire.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/agent-credential-expiry-pattern" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>The payment authorization pattern: giving an agent a wallet without giving it your bank account</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:15:02 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/the-payment-authorization-pattern-giving-an-agent-a-wallet-without-giving-it-your-bank-account-bh1</link>
      <guid>https://dev.to/draganristicrsjpg/the-payment-authorization-pattern-giving-an-agent-a-wallet-without-giving-it-your-bank-account-bh1</guid>
      <description>&lt;h2&gt;
  
  
  § 01 · Why this matters now
&lt;/h2&gt;

&lt;p&gt;Agent-initiated payments stopped being hypothetical this year. Protocols for autonomous agent transactions are shipping with real adoption, and industry alliances are forming specifically around agent-driven commerce. If your agent roadmap touches purchasing, subscriptions, refunds, or vendor payments, even narrowly, you need an authorization architecture before you need a use case, because retrofitting one after the first bad transaction is much more expensive. For a worked example, read about &lt;a href="https://orbiresearch.com/lab/agent-approved-own-refund" rel="noopener noreferrer"&gt;an agent that approved its own refund&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;── The naive version ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · The naive version, and why it fails
&lt;/h2&gt;

&lt;p&gt;The naive approach gives the agent direct API access to a payment method, a stored card, a linked bank account, scoped to "this vendor" or "this category." It fails for the same reason unscoped file access fails: the agent's job is to interpret ambiguous instructions and act, and payment amounts are exactly the kind of number a misread instruction gets wrong. A scope restricts where money can go. It does nothing to restrict how much, how often, or whether a human should have seen this first.&lt;/p&gt;

&lt;p&gt;── The pattern ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · The pattern
&lt;/h2&gt;

&lt;p&gt;Separate "the agent can request a payment" from "the payment executes" with three independent limits, none of which the agent controls.&lt;/p&gt;

&lt;p&gt;1. A hard ceiling per transaction. Set below the largest legitimate transaction you expect, not above it. The agent should never be able to construct a request that clears this ceiling, the limit lives in the payment layer, not in the agent's instructions.&lt;/p&gt;

&lt;p&gt;2. A velocity limit, not just a size limit. One large payment and fifty small ones can both drain an account. Cap total spend per run, per day, and per vendor independently, the same way you'd rate-limit an API.&lt;/p&gt;

&lt;p&gt;3. A human gate above a threshold, by default. Anything over your comfort ceiling routes to an approval queue instead of failing or executing. This is the same approval-queue pattern we've written about for other high-stakes actions, payments are simply the category where skipping it is most expensive.&lt;/p&gt;

&lt;p&gt;── The wallet ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · What the wallet actually holds
&lt;/h2&gt;

&lt;p&gt;Don't connect the agent to your actual payment account. Fund a dedicated, capped wallet or virtual card that the agent transacts against, agent payment protocols and virtual-card issuers both support this now. The wallet's maximum balance becomes your real worst-case exposure, independent of any logic bug in the agent itself. If the wallet holds $500, a broken agent can lose you $500, not your operating account.&lt;/p&gt;

&lt;p&gt;── Reconciliation ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · Reconciliation is not optional
&lt;/h2&gt;

&lt;p&gt;Every agent-initiated payment needs a machine-readable record of why, which run, which instruction, which approval (if any) authorized it, logged before the payment clears, not reconstructed afterward from a payment processor's dashboard. When a payment looks wrong three weeks later, you need to answer "what did the agent think it was doing" without guessing.&lt;/p&gt;

&lt;p&gt;── Checklist ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · Checklist
&lt;/h2&gt;

&lt;p&gt;[ ] Does a hard per-transaction ceiling live in the payment layer, outside the agent's control?&lt;/p&gt;

&lt;p&gt;[ ] Is there a velocity limit, spend per run, per day, per vendor, not just a size limit?&lt;/p&gt;

&lt;p&gt;[ ] Does anything above your comfort threshold route to a human by default?&lt;/p&gt;

&lt;p&gt;[ ] Is the agent funded through a capped wallet or virtual card, never your main payment account?&lt;/p&gt;

&lt;p&gt;[ ] Is the "why" behind every payment logged at authorization time, not reconstructed later?&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;◆ An agent that can pay for something is an agent that can be wrong about money. Build the ceiling before you build the use case.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/agent-payment-authorization-pattern" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>The agent-to-agent circuit breaker: what changes when your agent calls someone else's agent</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Tue, 06 Oct 2026 07:15:02 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/the-agent-to-agent-circuit-breaker-what-changes-when-your-agent-calls-someone-elses-agent-3fno</link>
      <guid>https://dev.to/draganristicrsjpg/the-agent-to-agent-circuit-breaker-what-changes-when-your-agent-calls-someone-elses-agent-3fno</guid>
      <description>&lt;h2&gt;
  
  
  § 01 · The assumption that just broke
&lt;/h2&gt;

&lt;p&gt;Retry logic, timeouts, circuit breakers, the standard reliability toolkit, was designed around calling APIs with predictable failure modes. A 429 means slow down. A 500 means try again later. A timeout means the service is overloaded. None of that logic accounts for what happens when the thing you're calling is itself an agent: something that can fail slowly and confidently, returning a well-formed, plausible-looking wrong answer instead of an error code.&lt;/p&gt;

&lt;p&gt;── The wrong signal ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · Why a standard circuit breaker doesn't catch this
&lt;/h2&gt;

&lt;p&gt;A circuit breaker trips on error rate or latency. An agent-to-agent call that returns a confident, malformed, or subtly wrong result looks like a success to every metric a standard circuit breaker watches. The call didn't fail. It just didn't do what you needed, and your circuit breaker has no way to know that, because it's watching the wrong signal.&lt;/p&gt;

&lt;p&gt;── The pattern ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · The pattern
&lt;/h2&gt;

&lt;p&gt;Add a second failure category, quality failure, that trips independently of the standard error-rate breaker.&lt;/p&gt;

&lt;p&gt;1. Validate the shape before you trust the content. Every response from an external agent gets checked against an expected schema before anything downstream touches it. This catches a large share of quality failures cheaply, before you need any semantic judgment at all.&lt;/p&gt;

&lt;p&gt;2. Sample for semantic drift, don't just check the schema. Periodically, not on every call, that's too expensive, run a known-good test case through the external agent and compare the result against an expected answer. A schema-valid response that's drifted away from correct won't trip a shape check, but it will trip this.&lt;/p&gt;

&lt;p&gt;3. Trip the breaker on quality failures, not just errors. When validation or sampling detects a bad result, treat it exactly like a 500 for circuit-breaker purposes, back off, fall back, or fail safe. Don't let "technically returned a 200" override "the content was wrong."&lt;/p&gt;

&lt;p&gt;4. Never let one agent's failure silently propagate as another agent's fact. If agent A returns a low-confidence or failed-validation result, agent B should receive that as a flagged, uncertain input, not as ground truth to build the next decision on. Multi-agent systems that skip this step are how a false consensus forms: several agents individually operating "correctly" on a bad input none of them flagged.&lt;/p&gt;

&lt;p&gt;5. Keep a fallback that doesn't depend on the failing agent. A cached last-known-good result, a simpler deterministic rule, or a human escalation, anything other than "retry the same agent and hope."&lt;/p&gt;

&lt;p&gt;── Where it compounds ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · Where this matters most
&lt;/h2&gt;

&lt;p&gt;The risk compounds with every additional agent in the chain. A single agent-to-agent call with a quality circuit breaker is a safety net. A four-agent pipeline where only the last hop validates anything is a system where an early, unflagged error gets progressively more confident-looking at each stage, exactly the failure mode we've seen in production multi-agent deployments that reach a wrong answer with more certainty than any single agent in the chain actually earned.&lt;/p&gt;

&lt;p&gt;── Checklist ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · Checklist
&lt;/h2&gt;

&lt;p&gt;[ ] Does every agent-to-agent call validate response shape before use?&lt;/p&gt;

&lt;p&gt;[ ] Do you sample for semantic correctness, not just schema validity?&lt;/p&gt;

&lt;p&gt;[ ] Does a quality failure trip your circuit breaker the same way an error code does?&lt;/p&gt;

&lt;p&gt;[ ] Does a low-confidence result from one agent get passed downstream as uncertain, not as fact?&lt;/p&gt;

&lt;p&gt;[ ] Is there a fallback path that doesn't depend on the same agent that just failed?&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;◆ If your circuit breaker only watches HTTP status codes, it's blind to the failure mode that actually costs you in a multi-agent system.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/agent-to-agent-circuit-breaker" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
    <item>
      <title>What goes in an approval request: a short reason or a link to the trace?</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Fri, 02 Oct 2026 07:15:02 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/what-goes-in-an-approval-request-a-short-reason-or-a-link-to-the-trace-4i68</link>
      <guid>https://dev.to/draganristicrsjpg/what-goes-in-an-approval-request-a-short-reason-or-a-link-to-the-trace-4i68</guid>
      <description>&lt;p&gt;A reader left a comment on the approval queue post: should the request carry a short justification inline, or just link to the trace? Short answer: both, but they do different jobs, and they should never be mixed together in one block of text. Here is how we lay it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 01 · Two ways an approval request goes wrong
&lt;/h2&gt;

&lt;p&gt;◆ Link only. The reviewer has to open a trace, scroll through tool calls and work out what happened before they can decide. Nobody does that forty times a day. They start approving from the title. The queue still exists, but it has stopped being a control.&lt;/p&gt;

&lt;p&gt;◆ Reason only. The agent writes a tidy paragraph explaining why the action is fine. That paragraph is written by the same system that wants the action approved. It can be wrong, and it is usually persuasive. A reviewer who reads only that is being talked into the answer.&lt;/p&gt;

&lt;p&gt;── ◆ ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · Split the request into three parts
&lt;/h2&gt;

&lt;p&gt;◆ Facts the system computed. Amount, recipient, which record changes, whether it can be undone, and which policy rule sent it to a human. The agent does not write any of this. The gate does, from the tool call itself. This is the part the reviewer decides on. When the agent writes the amount instead, you get &lt;a href="https://orbiresearch.com/lab/agent-approved-own-refund" rel="noopener noreferrer"&gt;an agent that approved its own refund&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;◆ The agent's reason, two lines at most. Labelled as the agent's claim. It shows intent, but it never carries the decision.&lt;/p&gt;

&lt;p&gt;◆ A link to the trace. For the cases where the facts and the reason do not match, or something looks off. Most reviews should not need it. If more than one in five do, the facts section is missing something.&lt;/p&gt;

&lt;p&gt;◆ On top of that, every request shows its deadline and what happens if nobody answers, as covered in the post on approval timeouts.&lt;/p&gt;

&lt;p&gt;── ◆ ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · What it looks like
&lt;/h2&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;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# computed by the gate from the tool call, never by the agent
&lt;/span&gt;    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "refund.issue"
&lt;/span&gt;    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "order 88214"
&lt;/span&gt;    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;          &lt;span class="c1"&gt;# "EUR 340.00"
&lt;/span&gt;    &lt;span class="n"&gt;reversible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;     &lt;span class="c1"&gt;# False
&lt;/span&gt;    &lt;span class="n"&gt;triggered_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;    &lt;span class="c1"&gt;# "amount above the 250 auto-approve limit"
&lt;/span&gt;    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;      &lt;span class="c1"&gt;# "2026-09-30T09:00Z"
&lt;/span&gt;    &lt;span class="n"&gt;on_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;      &lt;span class="c1"&gt;# "reject"
&lt;/span&gt;
    &lt;span class="c1"&gt;# written by the agent, shown as a claim
&lt;/span&gt;    &lt;span class="n"&gt;agent_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;    &lt;span class="c1"&gt;# trimmed to 200 characters
&lt;/span&gt;
    &lt;span class="c1"&gt;# for the exceptions
&lt;/span&gt;    &lt;span class="n"&gt;trace_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;undoable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversible&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;return &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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amount: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;   Undoable: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;undoable&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Why it needs you: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;triggered_by&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent says: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_reason&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If nobody answers by &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_timeout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Full trace: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trace_url&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 order matters. The facts come first and the agent's words come after them, with a label that says whose words they are.&lt;/p&gt;

&lt;p&gt;── ◆ ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · Check that people actually read it
&lt;/h2&gt;

&lt;p&gt;◆ Time to decision. If most approvals arrive within two or three seconds, nobody is reading. Track it per reviewer, not just as an average.&lt;/p&gt;

&lt;p&gt;◆ Rejection and edit rate. A queue with zero rejections over several weeks is either a perfect agent or a rubber stamp. It is rarely the first.&lt;/p&gt;

&lt;p&gt;◆ Planted requests. Once in a while, put in a request that should be rejected, with a wrong amount or the wrong recipient, and see whether it gets through. Tell reviewers that this happens, just not when. It is the only test that measures the control itself and not the agent.&lt;/p&gt;

&lt;p&gt;── ◆ ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · When to leave the reason out
&lt;/h2&gt;

&lt;p&gt;◆ For high value or hard to undo actions, consider showing the agent's reason only after the reviewer has looked at the facts, or dropping it altogether. A reason read first becomes the frame for everything after it.&lt;/p&gt;

&lt;p&gt;◆ We do not have data on where that line should sit. We would start by showing it below the facts for every action type and tighten from there, using the planted requests to see whether it changes what gets through.&lt;/p&gt;

&lt;p&gt;── ◆ ──&lt;/p&gt;

&lt;p&gt;Facts from the system first. The agent's reason second, short and labelled as a claim. A trace link for the exceptions. A default for when nobody answers. Then measure whether anyone is actually reading.&lt;/p&gt;

&lt;p&gt;The reference code for approval gates is in &lt;a href="https://github.com/draganristicrs-jpg/agent-reliability-patterns" rel="noopener noreferrer"&gt;agent-reliability-patterns&lt;/a&gt; on GitHub, MIT licensed.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/approval-request-contents" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Approval timeouts: what your agent should do when nobody answers</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Tue, 29 Sep 2026 21:17:51 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/approval-timeouts-what-your-agent-should-do-when-nobody-answers-3bjh</link>
      <guid>https://dev.to/draganristicrsjpg/approval-timeouts-what-your-agent-should-do-when-nobody-answers-3bjh</guid>
      <description>&lt;h2&gt;
  
  
  § 01 · The missing fifth field
&lt;/h2&gt;

&lt;p&gt;In the approval queue pattern I described four fields every approval item should carry: the action, the justification, the blast radius, and the confidence with an alternative. A reader on dev.to pointed out what was missing, and they were right. None of those fields answer a simple question: what happens when nobody answers?&lt;/p&gt;

&lt;p&gt;Every real queue has items that age. People are in meetings, on holiday, or just busy. If you don't decide up front what an expired item turns into, the system decides for you, and it usually decides badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · How queues rot without a timeout policy
&lt;/h2&gt;

&lt;p&gt;There are two common ways this goes wrong.&lt;/p&gt;

&lt;p&gt;◆ The graveyard. Nothing happens on timeout. Items pile up, the agent stalls, and after a few weeks people learn that the queue is where requests go to die. They stop looking at it, which is the opposite of what you built it for.&lt;/p&gt;

&lt;p&gt;◆ The global auto-approve. Someone gets tired of the backlog and adds a rule: anything older than 24 hours gets approved. It feels practical. It also means the most dangerous item in the queue, the one nobody felt comfortable approving, now goes through automatically. You have rebuilt the rubber stamp, just with a delay.&lt;/p&gt;

&lt;p&gt;In the original post I wrote that an SLA breach should escalate to a person and never auto-approve. I'd now make that more precise. For irreversible actions, that still holds. For some action types, a different default is the better answer.&lt;/p&gt;

&lt;p&gt;── Defaults by reversibility ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · One default per action type
&lt;/h2&gt;

&lt;p&gt;The fix is to give each action type its own timeout and its own default action, and to choose that default by asking one question: how expensive is it to undo this if the default turns out to be wrong?&lt;/p&gt;

&lt;p&gt;In practice four defaults cover almost everything.&lt;/p&gt;

&lt;p&gt;◆ Approve. For actions that are internal, reversible and low impact. Posting a summary to an internal dashboard, tagging a ticket, saving a draft. If nobody objects in time, the cost of doing it is close to zero and easy to reverse.&lt;/p&gt;

&lt;p&gt;◆ Hold and escalate. For actions that leave your system or move money. Sending an email to a customer, issuing a refund, paying an invoice. These never go through on silence. On timeout, the item goes to the next person on the escalation list and keeps waiting.&lt;/p&gt;

&lt;p&gt;◆ Cancel. For actions whose value expires. Booking a slot for today, replying to a quote request after the customer's deadline, reacting to a price that has already changed. Doing them late can be worse than not doing them. On timeout, the item is closed and the requester is told.&lt;/p&gt;

&lt;p&gt;◆ Re-plan. For actions built on data that may now be stale. If a proposal was based on a stock level, a balance or a status from yesterday, approving it today can be wrong even if it was right when it was created. On timeout, the item goes back to the agent, which checks the facts again and creates a fresh request if it still makes sense.&lt;/p&gt;

&lt;p&gt;The human who owns the queue picks the default per action type, in writing, before anything goes live. It is a product decision, not an engineering detail.&lt;/p&gt;

&lt;p&gt;── Picking the numbers ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · Timeouts are per type, not global
&lt;/h2&gt;

&lt;p&gt;One global SLA does not work, because a refund and a dashboard update do not deserve the same patience. Start with rough values per action type, for example 30 minutes for time sensitive customer replies, 4 working hours for refunds and 24 hours for internal changes, then tune them from real data.&lt;/p&gt;

&lt;p&gt;The useful numbers are the median and the 90th percentile of how long approvals actually take for each type. If the 90th percentile is longer than the timeout, the timeout will fire on normal days, and people will start to ignore it. If the median is only a few seconds, reviewers are probably not reading, which is a different problem and a more serious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · A small implementation
&lt;/h2&gt;

&lt;p&gt;The policy is small enough to live next to the routing rules. A minimal version:&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;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;APPROVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ESCALATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CANCEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REPLAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
    &lt;span class="n"&gt;on_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OnTimeout&lt;/span&gt;
    &lt;span class="n"&gt;escalate_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="n"&gt;POLICIES&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;internal_dashboard_post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APPROVE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&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="n"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_lead&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;refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finance_lead&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;same_day_booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CANCEL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stock_reorder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;OnTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REPLAN&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;resolve_expired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POLICIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_type&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;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# still waiting
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;escalate_to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than the code. Any action type that is not in the table should fall back to ESCALATE, never to APPROVE. And every decision made by a timeout has to be logged as a timeout decision, with the policy that caused it, so that an audit can tell the difference between "a person approved this" and "nobody looked and the clock approved it".&lt;/p&gt;

&lt;p&gt;── What you get ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · What changes when you do this
&lt;/h2&gt;

&lt;p&gt;The queue stays short. Low stakes items drain on their own, expired time sensitive items close cleanly, and stale ones get rechecked instead of approved blind. The only items that actually age are the irreversible ones, which are exactly the ones you want a person to look at.&lt;/p&gt;

&lt;p&gt;That is also why reviewers keep reading. When the queue only holds decisions that matter, people treat it with the attention it needs.&lt;/p&gt;

&lt;p&gt;If you want to try this, add a fifth column to the table in your APPROVALS.md: the timeout and the default for each action type. It takes an hour to write and saves a lot of arguments later.&lt;/p&gt;

&lt;p&gt;Thanks to the reader who raised this. The reference implementation of the other patterns is on GitHub (&lt;a href="https://github.com/draganristicrs-jpg/agent-reliability-patterns" rel="noopener noreferrer"&gt;agent-reliability-patterns&lt;/a&gt;), and the original post is here (&lt;a href="https://orbiresearch.com/lab/approval-queue-pattern" rel="noopener noreferrer"&gt;The approval queue pattern&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;◆ Every action type needs a timeout and a default: approve, hold and escalate, cancel, or re-plan.&lt;/p&gt;

&lt;p&gt;◆ Choose the default by how expensive the action is to undo. Anything unknown escalates.&lt;/p&gt;

&lt;p&gt;Log timeout decisions separately from human ones. "Nobody objected" is not the same as "someone approved".&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/approval-queue-timeout-defaults" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Running Hermes Agent in production: what changes when the agent works for a business, not for you</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Tue, 29 Sep 2026 21:17:12 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/running-hermes-agent-in-production-what-changes-when-the-agent-works-for-a-business-not-for-you-3alc</link>
      <guid>https://dev.to/draganristicrsjpg/running-hermes-agent-in-production-what-changes-when-the-agent-works-for-a-business-not-for-you-3alc</guid>
      <description>&lt;p&gt;Hermes Agent, the open-source agent from Nous Research, is designed around a simple idea: one agent, one memory, available everywhere, terminal, Telegram, Slack, WhatsApp, email, and getting more capable the longer it runs, because it turns solved problems into reusable skills. For a single developer that is exactly the right design. For a business deployment, several of those defaults need a second look. This is the checklist we use when Hermes is the runtime behind an agent that does real work for a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 01 · One client, one environment
&lt;/h2&gt;

&lt;p&gt;Hermes keeps its state, configuration, sessions, memories and skills, under a data directory (~/.hermes by default, or HERMES_HOME). That makes isolation straightforward to reason about: never let two clients share a data directory, and preferably never let them share a machine. We run each client on its own server. It costs slightly more than a multi-tenant setup, but it removes an entire class of failures, one client's memory or skills leaking into another client's conversations, and it makes data deletion at the end of an engagement a matter of destroying one environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · Choose the execution backend deliberately
&lt;/h2&gt;

&lt;p&gt;Hermes can run commands through several backends, local, Docker, SSH, Singularity and Modal, and the docs describe container hardening and namespace isolation for the sandboxed options. For a production agent that executes tools on behalf of a business, the local backend is the wrong default: a tool call that goes wrong should hit a container, not the host that also holds credentials and the agent's memory. Pick a sandboxed backend, give it only the network access the workflow needs, and treat the host as something the agent should never need to touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · Secrets never live in the conversation
&lt;/h2&gt;

&lt;p&gt;Messaging gateways make it tempting to hand the agent a credential in chat. Don't. Anything pasted into a conversation can end up in session history and memory. Keep API keys in environment configuration on the server, scope each one to the minimum the workflow needs, and rotate them on a schedule, the same rule you would apply to any service account.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · Plan for model failure, not just model choice
&lt;/h2&gt;

&lt;p&gt;Hermes is model-agnostic: it works with Nous Portal, OpenRouter, direct provider APIs or your own endpoint, and it supports routing and fallback configurations. Use that. A production agent should have a primary model and at least one fallback from a different provider, so a provider outage or rate limit degrades the agent instead of stopping it. Then test the fallback path on purpose, a fallback nobody has exercised is a fallback that doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · Put a human gate in front of irreversible actions
&lt;/h2&gt;

&lt;p&gt;Hermes can act autonomously and run scheduled tasks unattended through its gateway. That's powerful and it's exactly why the actions that can't be undone, sending money, emailing customers, deleting records, publishing, should route through an approval step. In our deployments that step is usually a message in the client's Telegram or Slack with the proposed action and an approve/reject choice. The agent does the work; a person owns the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · Memory is a feature and a liability
&lt;/h2&gt;

&lt;p&gt;Persistent memory is what makes Hermes useful over weeks instead of minutes. In a business setting it also means that a wrong fact, an outdated price or a one-off instruction can persist and quietly shape future answers. Decide up front what the agent is allowed to remember, review what it has stored on a schedule, and make sure you can delete a specific memory when a client asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 07 · Back up the data directory
&lt;/h2&gt;

&lt;p&gt;Everything the agent has learned lives in that data directory. Back it up like a database, on a schedule, off the machine, and test a restore. An agent that loses its skills and memory after a disk failure is back to day one.&lt;/p&gt;

&lt;p&gt;── Checklist ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 08 · Checklist
&lt;/h2&gt;

&lt;p&gt;[ ] Does every client have its own environment and data directory?&lt;/p&gt;

&lt;p&gt;[ ] Are tools executed in a sandboxed backend, not on the host?&lt;/p&gt;

&lt;p&gt;[ ] Are all credentials in server configuration, never in chat?&lt;/p&gt;

&lt;p&gt;[ ] Is there a fallback model from a different provider, and has it been tested?&lt;/p&gt;

&lt;p&gt;[ ] Do irreversible actions require human approval?&lt;/p&gt;

&lt;p&gt;[ ] Is there a policy for what the agent may remember, and a way to delete it?&lt;/p&gt;

&lt;p&gt;[ ] Is the data directory backed up and has a restore been tested?&lt;/p&gt;

&lt;p&gt;── End of note ──&lt;/p&gt;

&lt;p&gt;For how we define an agent's identity and boundaries before any of this, see &lt;a href="https://orbiresearch.com/lab/anatomy-of-a-soul-md" rel="noopener noreferrer"&gt;Anatomy of a SOUL.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/hermes-agent-production-deployment" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>research</category>
    </item>
    <item>
      <title>The approval queue pattern: putting a human in the loop without putting them in the way</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:00:56 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/the-approval-queue-pattern-putting-a-human-in-the-loop-without-putting-them-in-the-way-3ldl</link>
      <guid>https://dev.to/draganristicrsjpg/the-approval-queue-pattern-putting-a-human-in-the-loop-without-putting-them-in-the-way-3ldl</guid>
      <description>&lt;h2&gt;
  
  
  § 01 · The problem with "human in the loop"
&lt;/h2&gt;

&lt;p&gt;Every serious agent deployment ends up with a human in the loop somewhere. The phrase has become a checkbox. It is treated as a safety property you either have or do not have. In production it is neither binary nor free.&lt;/p&gt;

&lt;p&gt;A human in the loop has a cost, and the cost is latency plus attention. If the agent escalates everything, the human becomes the bottleneck the agent was supposed to remove. If the agent escalates nothing, the human is decoration. Most teams land in a worse place than either: the agent escalates an unpredictable mix of trivial and critical items, the human learns that most items are trivial, and they start approving without reading. Now you have a human in the loop who is functionally not in the loop. You have built a rubber stamp and called it governance.&lt;/p&gt;

&lt;p&gt;The approval queue pattern exists to make the human expensive on purpose, and to spend that expense only where it changes the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · The core idea
&lt;/h2&gt;

&lt;p&gt;An approval queue is a durable, ordered list of proposed actions that the agent has decided it should not execute on its own authority. The agent does the reasoning and the drafting. It stops at the action. A human approves, edits, or rejects. The decision and the human who made it are recorded.&lt;/p&gt;

&lt;p&gt;Three properties make this a pattern rather than a button:&lt;/p&gt;

&lt;p&gt;◆ It is durable. An approval item survives a crash, a restart, and the human going home for the weekend. It lives in a table, not in memory.&lt;/p&gt;

&lt;p&gt;◆ It is ordered and bounded. Items have priority and an age. An item that sits unapproved past its SLA is itself an event that triggers escalation, not silence.&lt;/p&gt;

&lt;p&gt;◆ It is reversible at the boundary, not after. The human decides before the irreversible action, not after a notification that it already happened.&lt;/p&gt;

&lt;p&gt;The unit of the queue is the proposed action, never the conversation. You are not asking a human to review a transcript. You are asking them to approve one specific, executable thing.&lt;/p&gt;

&lt;p&gt;── The four fields ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · The four fields every approval item must carry
&lt;/h2&gt;

&lt;p&gt;An approval item that contains only "the agent wants to do X, approve?" forces the human to reconstruct context they do not have. They will either over-investigate (slow) or approve blind (dangerous). Every item must carry four fields.&lt;/p&gt;

&lt;p&gt;◆ &lt;strong&gt;The action.&lt;/strong&gt; The exact, executable operation, in plain language and in its concrete form. Not "send outreach." Instead: "Send this email, shown in full, to this address." The human approves the artifact, not a description of it.&lt;/p&gt;

&lt;p&gt;◆ &lt;strong&gt;The justification.&lt;/strong&gt; Why the agent proposes this now. The trigger, the rule, the data that led here. One or two sentences. If the agent cannot state why, that is itself a reason to reject.&lt;/p&gt;

&lt;p&gt;◆ &lt;strong&gt;The blast radius.&lt;/strong&gt; What this action touches and how hard it is to undo. "One outbound email, not recallable" is a different decision than "one row update, fully reversible." The human is pricing risk. Give them the price.&lt;/p&gt;

&lt;p&gt;◆ &lt;strong&gt;The confidence and the alternative.&lt;/strong&gt; What the agent would do if this were rejected, and how sure it is. A low-confidence proposal with an obvious fallback is a fast approve or fast reject. A high-confidence proposal with no fallback deserves the human's full attention.&lt;/p&gt;

&lt;p&gt;These four fields turn a vague ask into a decision a busy person can make in seconds without becoming a rubber stamp.&lt;/p&gt;

&lt;p&gt;── What belongs in the queue ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · What goes in the queue, and what does not
&lt;/h2&gt;

&lt;p&gt;The discipline is in what you do not route. An approval queue that contains everything is a denial-of-service attack on your own operators.&lt;/p&gt;

&lt;p&gt;Route to the queue when at least one is true: the action is irreversible or expensive to undo, the action is externally visible (a customer sees it, money moves, a record leaves your system), or the agent's confidence is below a threshold you set per action class.&lt;/p&gt;

&lt;p&gt;Do not route when the action is internal, reversible, and inside the agent's defined scope. Reading data, drafting, summarizing, updating a record the agent owns and can roll back: these are inside the boundary. If you route them, you train your operators to stop reading, and the one item that mattered slips through behind forty that did not.&lt;/p&gt;

&lt;p&gt;The threshold is a dial, not a default. Set it per action class. Outbound customer communication might require approval at any confidence. An internal tag update might require approval only below 70 percent. Write the thresholds down. They are part of the system, not a runtime guess.&lt;/p&gt;

&lt;p&gt;── The failure modes ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · The failure modes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rubber stamp.&lt;/strong&gt; The queue fills with low-stakes items, the operator approves in bulk, and the queue stops being a control. Fix: aggressively reduce what enters the queue, and measure approval time per item. If median approval time drops below the time it takes to read the action, your operators are not reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue as graveyard.&lt;/strong&gt; Items pile up unapproved because no one owns the queue. The agent stalls or, worse, starts taking the unapproved actions because a timeout was wired to "proceed." Fix: every queue has an owner and an SLA, and the SLA breach escalates to a person, never to auto-approve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context starvation.&lt;/strong&gt; Operators approve blind because the four fields are missing or thin. Fix: treat a proposal with a weak justification as a defect in the agent, not a judgment call for the human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent scope creep.&lt;/strong&gt; The set of actions that bypass the queue grows over time, each addition reasonable, until the agent is doing things no one is reviewing. Fix: the routing rules live in version control and are reviewed on the same cadence as the trust boundary.&lt;/p&gt;

&lt;p&gt;── The file that holds the queue ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · The contract behind the queue
&lt;/h2&gt;

&lt;p&gt;Create an &lt;strong&gt;APPROVALS.md&lt;/strong&gt; in the agent repository. It lists every action class the agent can propose, the routing rule for each (always, never, or below a confidence threshold), the SLA for each priority level, the escalation target when an SLA is breached, and the owner of the queue. It records the last review date.&lt;/p&gt;

&lt;p&gt;This is the document your operations lead reads on day one and your auditor reads on the worst day. An approval queue without a written contract is not a control. It is a habit, and habits drift.&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;◆ A human in the loop is a cost. Spend it only where it changes the outcome.&lt;/p&gt;

&lt;p&gt;◆ The unit of the queue is one executable action with four fields, never a transcript.&lt;/p&gt;

&lt;p&gt;If your median approval time is shorter than the time to read the action, you do not have a human in the loop. You have a rubber stamp.&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/approval-queue-pattern" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Retry, Backoff, and Circuit Breakers for LLM API Calls</title>
      <dc:creator>orbiresearch</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:00:20 +0000</pubDate>
      <link>https://dev.to/draganristicrsjpg/retry-backoff-and-circuit-breakers-for-llm-api-calls-h3k</link>
      <guid>https://dev.to/draganristicrsjpg/retry-backoff-and-circuit-breakers-for-llm-api-calls-h3k</guid>
      <description>&lt;p&gt;Retry LLM API calls on 429, 5xx, and timeouts with exponential backoff plus jitter, honor the Retry-After header when the provider sends one, cap total retry time with a deadline, and wrap the whole thing in a circuit breaker so a degraded provider cannot take your system down with it. Never retry non-idempotent actions blindly, and never retry validation errors at all. That is the summary. The rest of this article is why each rule exists, what the naive version costs, and the code shape we ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 01 · LLM APIs fail differently
&lt;/h2&gt;

&lt;p&gt;Every API fails. LLM APIs fail with personality. Four modes dominate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits (429).&lt;/strong&gt; The most common and the most misunderstood. A 429 is not an error in your code, it is the provider telling you to slow down. We once treated it as fatal and it cost us a night of production downtime, the full story is in our &lt;a href="https://orbiresearch.com/lab/agent-crashed-on-429" rel="noopener noreferrer"&gt;429 postmortem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overload (529 and friends).&lt;/strong&gt; Provider-side congestion. Distinct from 429 because slowing down helps your standing but the recovery timeline is not yours to control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts.&lt;/strong&gt; Long generations on loaded infrastructure can exceed any sane client timeout. The dangerous part: a timeout does not mean the request failed. The provider may have completed it after you hung up, which matters enormously if the call had side effects downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Degraded output.&lt;/strong&gt; The sneakiest mode: the API returns 200 and the content is wrong, truncated, or malformed. No retry logic sees this unless your validation layer feeds it. Silent regressions are their own topic, and we &lt;a href="https://orbiresearch.com/lab/agent-silent-model-regression" rel="noopener noreferrer"&gt;wrote one&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;── The retry ladder ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 02 · The retry ladder
&lt;/h2&gt;

&lt;p&gt;The first decision is not how to retry, it is whether.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry freely:&lt;/strong&gt; 429, 5xx, network errors, timeouts on read-only calls. These are transient by definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry carefully:&lt;/strong&gt; timeouts on calls whose results trigger side effects. The request may have succeeded invisibly. Before retrying, the system needs an idempotency story, a way to guarantee the side effect happens once even if the call happens twice. We &lt;a href="https://orbiresearch.com/lab/agent-double-charged-customers-idempotency" rel="noopener noreferrer"&gt;learned that one expensively&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never retry:&lt;/strong&gt; 400s, validation errors, authentication failures, content policy refusals. The request is wrong, and resending a wrong request is paying twice for the same no. Retrying a 401 in a loop is how you turn an expired key into a locked account.&lt;/p&gt;

&lt;p&gt;── Backoff, jitter, and the Retry-After header ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 03 · Backoff, jitter, and the Retry-After header
&lt;/h2&gt;

&lt;p&gt;Immediate retries against a rate-limited endpoint are a self-inflicted denial of service: every client that failed at second zero retries at second one, together, and the stampede keeps the endpoint saturated. Two mechanisms break the stampede.&lt;/p&gt;

&lt;p&gt;Exponential backoff spreads retries over time: wait 1s, then 2s, then 4s, then 8s. Jitter spreads them across clients: randomize each wait so a thousand failed requests do not become a thousand synchronized retries. And when the provider sends a Retry-After header, that number wins over your formula, it is the provider telling you exactly when to come back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAttempts&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTotalMs&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="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="nx"&gt;llm&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry-after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// jitter&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// budget exhausted&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in that snippet carry most of the value. The deadline: retries consume time your user or your queue is waiting through, and unbounded patience is not resilience, it is a hung system with good intentions. And the cap on backoff: waiting 512 seconds because the formula says so helps nobody.&lt;/p&gt;

&lt;p&gt;── Circuit breakers, or knowing when to stop knocking ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 04 · Circuit breakers, or knowing when to stop knocking
&lt;/h2&gt;

&lt;p&gt;Retries handle a request that failed. Circuit breakers handle a provider that is failing. The difference matters: when an endpoint is down for ten minutes, ten thousand well-behaved retrying requests are still ten thousand requests achieving nothing, burning your rate limits, your latency budgets, and your queue depth.&lt;/p&gt;

&lt;p&gt;A circuit breaker watches the failure rate. When it crosses a threshold, the circuit opens: calls fail fast without touching the API. After a cooldown, a few probe requests test the water. Success closes the circuit, failure keeps it open. Three states, one job: stop paying for calls that cannot succeed.&lt;/p&gt;

&lt;p&gt;In agent systems the breaker earns its keep twice over, because agents retry at the task level too. An agent that cannot reach its model will often rephrase, replan, and try again, multiplying the underlying API calls. Without a breaker, one degraded provider turns a patient agent into a very expensive metronome. Breaker state changes belong on your dashboard, and if they surprise you there, &lt;a href="https://orbiresearch.com/lab/instrumentation-pattern" rel="noopener noreferrer"&gt;your instrumentation has gaps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;── The queue is your shock absorber ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 05 · The queue is your shock absorber
&lt;/h2&gt;

&lt;p&gt;Everything above assumes a request in flight. The system-level question is what happens to work that arrives while the provider is down. The answer that survives production: a persistent queue. Requests land in the queue, workers drain it through the retry and breaker machinery, and a provider outage becomes a growing queue instead of lost work. When the provider recovers, the queue drains, and nothing that mattered disappeared during the gap. Every system we ship is built on this shape, most visibly the &lt;a href="https://orbiresearch.com/case-studies/logistics-fleet" rel="noopener noreferrer"&gt;nine-agent logistics fleet&lt;/a&gt;, where a restart or an outage resumes from the last confirmed message.&lt;/p&gt;

&lt;p&gt;── The fallback model is uptime engineering ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 06 · The fallback model is uptime engineering
&lt;/h2&gt;

&lt;p&gt;This month a frontier model vanished from the market for twenty days on a government decision. No retry policy fixes that. The last layer of this stack is a fallback model: a second provider or a smaller model, pre-tested against your workload, behind the same interface. Not because the fallback is as good, but because degraded service beats no service, and because the switch has to be a config change, not an engineering sprint that starts the morning of the outage.&lt;/p&gt;

&lt;p&gt;── The checklist ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 07 · The checklist
&lt;/h2&gt;

&lt;p&gt;[ ] 429 and 5xx retried with exponential backoff and jitter&lt;/p&gt;

&lt;p&gt;[ ] Retry-After header honored over the local formula&lt;/p&gt;

&lt;p&gt;[ ] Total retry time capped by a deadline, per request&lt;/p&gt;

&lt;p&gt;[ ] Non-idempotent calls have an idempotency key before any retry&lt;/p&gt;

&lt;p&gt;[ ] 400s, auth failures, and refusals are never retried&lt;/p&gt;

&lt;p&gt;[ ] Circuit breaker per provider, state visible on the dashboard&lt;/p&gt;

&lt;p&gt;[ ] Persistent queue in front of the model, drains after outages&lt;/p&gt;

&lt;p&gt;[ ] Fallback model tested against the real workload, switchable by config&lt;/p&gt;

&lt;p&gt;── Closing ──&lt;/p&gt;

&lt;h2&gt;
  
  
  § 08 · Closing
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Backoff, breakers, queues, and fallbacks predate language models by decades, and the teams that treat an LLM endpoint like any other unreliable dependency get boring, reliable systems. The teams that treat it as magic get to write postmortems. We know, we wrote ours, and building so the next one never happens is most of what &lt;a href="https://orbiresearch.com/cases" rel="noopener noreferrer"&gt;production-grade&lt;/a&gt;means.&lt;/p&gt;

&lt;p&gt;── End of pattern ──&lt;/p&gt;

&lt;p&gt;ORBIRESEARCH&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://orbiresearch.com/lab/llm-api-retry-backoff-circuit-breakers" rel="noopener noreferrer"&gt;OrbiResearch Lab&lt;/a&gt;. We build production AI agents at &lt;a href="https://orbiresearch.com" rel="noopener noreferrer"&gt;orbiresearch.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>reliability</category>
    </item>
  </channel>
</rss>
