<?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: Goutham Nishkal Deepu EDA</title>
    <description>The latest articles on DEV Community by Goutham Nishkal Deepu EDA (@goutham_nishkaldeepueda).</description>
    <link>https://dev.to/goutham_nishkaldeepueda</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%2F1728018%2Fc8473fd1-052a-4d5c-9fac-c7185f13e953.jpg</url>
      <title>DEV Community: Goutham Nishkal Deepu EDA</title>
      <link>https://dev.to/goutham_nishkaldeepueda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goutham_nishkaldeepueda"/>
    <language>en</language>
    <item>
      <title>Human-in-the-Loop Is Not a Governance Strategy</title>
      <dc:creator>Goutham Nishkal Deepu EDA</dc:creator>
      <pubDate>Fri, 10 Jul 2026 20:34:31 +0000</pubDate>
      <link>https://dev.to/goutham_nishkaldeepueda/human-in-the-loop-is-not-a-governance-strategy-157g</link>
      <guid>https://dev.to/goutham_nishkaldeepueda/human-in-the-loop-is-not-a-governance-strategy-157g</guid>
      <description>&lt;p&gt;Picture the human at the end of an agent pipeline.&lt;/p&gt;

&lt;p&gt;The agent drafts an action. A modal pops up. Approve or reject. The human has seen forty of these today, they all looked fine, and the queue is backing up. They click approve. They will click approve on the next one too.&lt;/p&gt;

&lt;p&gt;That is "human-in-the-loop." It is on almost every agentic-AI architecture diagram, usually drawn as a small person icon standing between the model and the irreversible action, labelled &lt;em&gt;governance&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It is not governance. It is a rubber stamp with a person's name on it, and its main function is to move liability from the system to the human who clicked. I want to argue that most HITL, as shipped, makes systems &lt;em&gt;look&lt;/em&gt; accountable while making them slightly less safe, and then describe what actually works instead, with code you can adapt.&lt;/p&gt;

&lt;p&gt;No prior background assumed beyond having built or used an agent that takes actions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The wrong question
&lt;/h2&gt;

&lt;p&gt;The phrase "human-in-the-loop" quietly smuggles in a claim: that the presence of a human constitutes oversight. So teams optimise for presence. Is there a human in the loop? Yes? Ship it.&lt;/p&gt;

&lt;p&gt;But presence is not the variable that matters. A human who is present but cannot realistically change the outcome is not a safeguard. They are decoration, and worse, they are &lt;em&gt;load-bearing&lt;/em&gt; decoration, because everyone downstream now assumes the action was reviewed. The audit log says "approved by a human." The incident review will say it too.&lt;/p&gt;

&lt;p&gt;The right question is not "is there a human in the loop?" It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the human positioned to actually change the outcome?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That reframe turns governance from a checkbox into a design problem. And design problems have structure. Three parts of it matter, and then a fourth thing that sits above all three.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Surface the decision, not the data
&lt;/h2&gt;

&lt;p&gt;The rubber-stamp modal usually shows the human &lt;em&gt;the action&lt;/em&gt;: here is the SQL we're about to run, here is the email we're about to send. Raw output. To approve it meaningfully, the human has to reconstruct the entire context the agent already had, in the two seconds before the queue guilt-trips them into clicking.&lt;/p&gt;

&lt;p&gt;Real review surfaces the &lt;em&gt;decision&lt;/em&gt;, not the payload. What is the agent trying to accomplish, what did it consider, what is it unsure about, and what happens if this is wrong?&lt;/p&gt;

&lt;p&gt;Concretely, that is the difference between handing the human this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_login&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'trial'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and handing them this:&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="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doing&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;delete duplicate trial user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;affects&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 record&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;risk&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;irreversible&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;agent_confidence&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;60%&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;if_wrong&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;restore from nightly backup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first asks the human to be a database reviewer at queue speed. The second asks them to make a judgment they are actually equipped to make: &lt;em&gt;is a 60%-confident irreversible delete of one record worth a human's attention right now?&lt;/em&gt; That is a question a tired person can answer well. Parsing a &lt;code&gt;DELETE&lt;/code&gt; filter for a subtle bug is not.&lt;/p&gt;

&lt;p&gt;If your HITL layer shows the human the same thing your logs show, you have built an audit trail, not an oversight step. The payload is for the log. The decision summary is for the human.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Interrupt on what matters, not on everything
&lt;/h2&gt;

&lt;p&gt;Here is the naive policy almost everyone ships. Anything that writes gets a human; anything that only reads runs free.&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;naive_needs_human&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&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;bool&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels safe. It is actually the source of the rubber stamp, because it interrupts &lt;em&gt;constantly&lt;/em&gt;, and constant interruption is how you train a human to approve reflexively. A gate that fires on every write is, functionally, a gate that fires on nothing, because the human behind it has stopped reading.&lt;/p&gt;

&lt;p&gt;A better policy interrupts on the properties that actually make an action dangerous: is it reversible, how large is the blast radius, and how confident is the agent in its own decision? And it recognises a third outcome beyond allow and review: some actions should never reach a human at all, because no realistic human review makes them safe.&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;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;Decision&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;ALLOW&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# run without asking
&lt;/span&gt;    &lt;span class="n"&gt;REVIEW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# surface to a human
&lt;/span&gt;    &lt;span class="n"&gt;BLOCK&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# never automate; architectural "no"
&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;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;reversible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;blast_radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;      &lt;span class="c1"&gt;# records or people affected
&lt;/span&gt;    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;      &lt;span class="c1"&gt;# agent's own confidence, 0..1
&lt;/span&gt;    &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;       &lt;span class="c1"&gt;# what the agent is trying to do
&lt;/span&gt;    &lt;span class="n"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;         &lt;span class="c1"&gt;# how it gets reversed, if it can be
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;govern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conf_floor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius_cap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hard_radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Top tier: irreversible AND huge -&amp;gt; not an approval question, an architecture one.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversible&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blast_radius&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hard_radius&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;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BLOCK&lt;/span&gt;
    &lt;span class="c1"&gt;# Reversible -&amp;gt; let it run; you can undo.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversible&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;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALLOW&lt;/span&gt;
    &lt;span class="c1"&gt;# Irreversible but bounded -&amp;gt; review only if big or uncertain.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blast_radius&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;radius_cap&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;conf_floor&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;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REVIEW&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALLOW&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a spread of actions through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read_file    -&amp;gt; allow
db_write     -&amp;gt; allow    (reversible, undo by id)
delete_row   -&amp;gt; review   (irreversible, only 60% confident)
send_email   -&amp;gt; block    (irreversible, 1,200 recipients)
wipe_table   -&amp;gt; block    (irreversible, 900 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what happened to the mass email. The naive policy would have shown it to a human for approval, which &lt;em&gt;feels&lt;/em&gt; responsible. But "email 1,200 customers, irreversibly, right now" is not a decision you want made by whoever happens to be watching the queue. It is a decision that belongs in the architecture, made once, cold, with a rate limit and a second sign-off path, not in a modal at 4pm. Routing it to &lt;code&gt;BLOCK&lt;/code&gt; is not ducking governance. It is putting the governance where it can be done well.&lt;/p&gt;

&lt;p&gt;The middle tier, &lt;code&gt;REVIEW&lt;/code&gt;, now fires rarely, on exactly the actions where a human's judgment is both needed and possible. Governance quality is not proportional to how often you stop. It is inversely proportional to how often you stop &lt;em&gt;for no reason&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Give the human a real "no"
&lt;/h2&gt;

&lt;p&gt;An approval step where "reject" has no defined path is not a decision, it is a formality. If rejecting the action just dead-ends the workflow, throws an error, or dumps the problem back on a user with no next step, everyone learns quickly that "reject" is the button that breaks things, and they stop pressing it.&lt;/p&gt;

&lt;p&gt;A real "no" needs somewhere to go. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fallback&lt;/strong&gt; the system runs instead (a safer, smaller version of the action).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A re-plan&lt;/strong&gt;, where rejection feeds back to the agent as a signal to try a different approach rather than halting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An escalation&lt;/strong&gt; to someone with more context or authority, not a dead end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt;, where the workflow completes in a reduced but coherent state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human also needs &lt;em&gt;time&lt;/em&gt; and &lt;em&gt;the ability to be right&lt;/em&gt;: enough context to form a judgment, and a system that treats their rejection as information rather than an obstacle. If your reject path is slower, riskier, or more painful for the human than the approve path, you have not built a decision point. You have designed the human into compliance, and the approve button is the only one that works.&lt;/p&gt;

&lt;p&gt;A quick test: pull up your agent's reject handler. If it is a &lt;code&gt;raise&lt;/code&gt; or a &lt;code&gt;return None&lt;/code&gt;, you do not have human-in-the-loop. You have human-in-the-way, and your operators already know it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The counterintuitive core
&lt;/h2&gt;

&lt;p&gt;Put the three together and you get a conclusion that surprises people: &lt;strong&gt;good human-in-the-loop design interrupts the human less, not more.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This runs against the instinct that more oversight is safer. But oversight has a capacity, and it is small. Every unnecessary approval spends down the human's attention and trains the reflex that makes the necessary approval worthless. A system that stops the human ten times a day for trivia and once a month for something that matters has all but guaranteed they will sleepwalk through the one that matters.&lt;/p&gt;

&lt;p&gt;There is a body of human-factors research behind this, under names like automation complacency and alarm fatigue: when a signal fires constantly and is almost always benign, operators stop attending to it, and they miss the rare real one. HITL modals are alarms. The same failure mode applies. A system that cries wolf on every database write has trained its humans to ignore the wolf.&lt;/p&gt;

&lt;p&gt;So concentrate the interruptions. Make each one rare, high-context, and consequential. That is not less governance. It is the only kind that survives contact with a tired human and a full queue.&lt;/p&gt;




&lt;h2&gt;
  
  
  A cleaner way to think about it: levels of autonomy
&lt;/h2&gt;

&lt;p&gt;The allow / review / block split hints at a more useful mental model than "is there a human in the loop." Think of each &lt;em&gt;action type&lt;/em&gt; your agent can take as sitting at one of a few autonomy levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 0, Blocked.&lt;/strong&gt; The agent may propose it, but a human must initiate it through a separate, deliberate path. Reserved for irreversible, high-blast-radius actions. The governance is architectural.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 1, Review.&lt;/strong&gt; The agent prepares the action and a decision summary; a human approves before execution, with a real reject path. Reserved for irreversible-but-bounded, or low-confidence actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2, Notify.&lt;/strong&gt; The agent acts autonomously but the action is reversible and logged loudly, so a human can catch and undo it after the fact. Most write actions live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3, Autonomous.&lt;/strong&gt; The agent acts freely. Reads, idempotent operations, anything with no meaningful blast radius.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real governance work is not building the modal. It is deciding, per action type, which level it belongs to, and being honest that most teams default everything to Level 1 (approve everything) because it feels safe and requires no thought. It is the least safe option that looks the safest. Sorting your actions into these tiers, deliberately, is the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;There is a case none of this solves, and pretending otherwise is how HITL became a liability blanket in the first place.&lt;/p&gt;

&lt;p&gt;Sometimes the right answer is not "add a human." It is "do not automate this action." If an action is irreversible, high-blast-radius, and the agent's confidence is not trustworthy, no approval modal fixes that, because you have handed a human a decision they cannot make well under the conditions you have given them. That is what Level 0 is for. At the top of the risk spectrum, the governance decision is made long before any human sees a modal: you simply keep the action out of the agent's autonomous reach.&lt;/p&gt;

&lt;p&gt;And even a well-designed review step has a ceiling. It assumes the human &lt;em&gt;can&lt;/em&gt; evaluate the decision in the time available. For genuinely complex actions, where correctness depends on context the human cannot absorb at a glance, "surface the decision" is not enough, and the answer is either better tooling to make the decision legible or a demotion to Level 0. HITL is a control for the middle of the spectrum. It is not a universal solvent, and treating it as one is the original mistake.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Is there a human in the loop?" is the wrong question, and it is dangerous precisely because it is so easy to answer yes.&lt;/p&gt;

&lt;p&gt;The questions that matter are harder. Can the human see the decision, or just the data? Are they interrupted for reasons, or reflexively? Do they have a real "no"? Which autonomy level does this action actually belong at? And should it have been automated at all?&lt;/p&gt;

&lt;p&gt;Human-in-the-loop is not a governance strategy. It is one control, useful in a specific band of risk, and only when it is designed so the human can actually govern. A person positioned to change nothing is not oversight. They are the place the accountability goes to disappear.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>machinelearning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Kalman Filter Is a Memory System</title>
      <dc:creator>Goutham Nishkal Deepu EDA</dc:creator>
      <pubDate>Fri, 10 Jul 2026 20:14:19 +0000</pubDate>
      <link>https://dev.to/goutham_nishkaldeepueda/a-kalman-filter-is-a-memory-system-4fee</link>
      <guid>https://dev.to/goutham_nishkaldeepueda/a-kalman-filter-is-a-memory-system-4fee</guid>
      <description>&lt;p&gt;Three problems that look unrelated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A vibration sensor on a motor gives you a jittery reading every second. You want to know the &lt;em&gt;actual&lt;/em&gt; wear on the bearing inside, which you can't measure directly.&lt;/li&gt;
&lt;li&gt;An AI agent has a long conversation with a user. It has to decide what to remember and what to drop, or it either forgets what you told it or drowns in its own history.&lt;/li&gt;
&lt;li&gt;A video model watches a ball roll behind a wall. When the ball reappears, the model has to &lt;em&gt;know it's the same ball&lt;/em&gt;, that it existed the whole time, unseen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are usually filed under three different fields: signal processing, LLM engineering, and computer vision. But they are the same problem. All three come down to one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given everything I've seen so far, and one new noisy observation, what do I now believe, and how much of my history do I keep versus throw away?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question has a sixty-year-old answer that most people learn as control theory and never think about again: the &lt;strong&gt;Kalman filter&lt;/strong&gt;. I want to show you that it isn't really a control-theory tool. It's the cleanest, oldest instance we have of a &lt;em&gt;memory system&lt;/em&gt;. And once you see it that way, the connection to agent memory and video models stops being a metaphor and becomes the same math wearing different clothes.&lt;/p&gt;

&lt;p&gt;No prior knowledge of Kalman filters or agent memory assumed. We'll build both from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup: a state you can't see
&lt;/h2&gt;

&lt;p&gt;Start with the bearing. There's a hidden quantity you actually care about. Call it the &lt;strong&gt;wear&lt;/strong&gt;, a number from 0 (brand new) to 1 (failed). You can't measure wear directly. You can only measure a &lt;em&gt;proxy&lt;/em&gt;: vibration, current draw, temperature. Those readings correlate with wear, but they're noisy. Any single reading might be high just because the sensor twitched.&lt;/p&gt;

&lt;p&gt;So you have two sources of information, and both are unreliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A model of how the world changes.&lt;/strong&gt; Wear only goes up, and slowly. That's a prediction: "whatever the wear was last second, it's about the same now, maybe a hair higher."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A measurement.&lt;/strong&gt; The sensor just gave you a number. It's informative, but noisy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Naively, you could just trust the sensor and plot the raw readings. But that throws away everything you knew a moment ago. Or you could trust your model and ignore the sensor, but then you're not learning from reality at all.&lt;/p&gt;

&lt;p&gt;The Kalman filter is the principled way to &lt;strong&gt;blend the two&lt;/strong&gt;, weighting each by how much you trust it. And that weight is the whole story.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two moves: predict and update
&lt;/h2&gt;

&lt;p&gt;A Kalman filter holds two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt;: its current best belief about the hidden state (the wear).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;P&lt;/code&gt;: its &lt;em&gt;uncertainty&lt;/em&gt; about that belief (how sure it is).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time step, it does two moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predict.&lt;/strong&gt; Carry the belief forward using the model of the dynamics. If wear persists, &lt;code&gt;x&lt;/code&gt; stays roughly where it was. But uncertainty &lt;em&gt;grows&lt;/em&gt;, because time passed and things could have drifted:&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;          &lt;span class="c1"&gt;# carry the state forward
&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;  &lt;span class="c1"&gt;# uncertainty grows by process noise Q
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Update.&lt;/strong&gt; A new measurement &lt;code&gt;z&lt;/code&gt; arrives. Fold it in, but only partly, weighted by a factor &lt;code&gt;K&lt;/code&gt; called the &lt;strong&gt;Kalman gain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# how much to trust the measurement vs. your belief
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# nudge belief toward the measurement, scaled by K
&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;        &lt;span class="c1"&gt;# uncertainty shrinks, you just learned something
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely at &lt;code&gt;K&lt;/code&gt;, because &lt;code&gt;K&lt;/code&gt; is a memory knob.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;R&lt;/code&gt; is the measurement noise, i.e. how unreliable the sensor is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;P&lt;/code&gt; is your current uncertainty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your belief is very uncertain (&lt;code&gt;P&lt;/code&gt; large) relative to the sensor noise, &lt;code&gt;K&lt;/code&gt; approaches 1: &lt;strong&gt;trust the new measurement, discard your prior.&lt;/strong&gt; If your belief is confident (&lt;code&gt;P&lt;/code&gt; small) and the sensor is noisy (&lt;code&gt;R&lt;/code&gt; large), &lt;code&gt;K&lt;/code&gt; approaches 0: &lt;strong&gt;keep your history, ignore the twitchy reading.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. That's the entire mechanism. &lt;code&gt;K&lt;/code&gt; is a continuously-adjusted answer to &lt;em&gt;"how much of the past do I carry forward, and how much of this new observation do I let overwrite it?"&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch it run
&lt;/h2&gt;

&lt;p&gt;Here's a complete 1D filter tracking bearing wear from a noisy sensor. It runs as-is:&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;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="c1"&gt;# True hidden wear: rises steadily toward failure (we don't get to see this)
&lt;/span&gt;&lt;span class="n"&gt;true_wear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Noisy sensor: correlates with wear but jittery
&lt;/span&gt;&lt;span class="n"&gt;measurements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true_wear&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&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="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;     &lt;span class="c1"&gt;# belief, and uncertainty in it
&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2e-3&lt;/span&gt;            &lt;span class="c1"&gt;# process noise: how much wear can drift per step
&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;         &lt;span class="c1"&gt;# measurement noise: how noisy the sensor is
&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;             &lt;span class="c1"&gt;# dynamics: wear persists
&lt;/span&gt;
&lt;span class="n"&gt;estimates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gains&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;for&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# PREDICT
&lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;
    &lt;span class="c1"&gt;# UPDATE
&lt;/span&gt;    &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# the memory knob
&lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;
    &lt;span class="n"&gt;estimates&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gains&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="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gain, first 3 steps:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gains&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; [0.994 0.566 0.468]
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gain, steady state :&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gains&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="c1"&gt;# -&amp;gt; 0.424
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting output isn't the wear estimate. It's the &lt;strong&gt;gain trajectory&lt;/strong&gt;: &lt;code&gt;0.994 → 0.566 → 0.468 → ... → 0.424&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the very first step, the filter knows nothing (&lt;code&gt;P&lt;/code&gt; is huge), so &lt;code&gt;K ≈ 0.99&lt;/code&gt;: it essentially adopts the first measurement wholesale. It has no memory yet, so it trusts the sensor completely. Then, step by step, as it accumulates history, &lt;code&gt;P&lt;/code&gt; shrinks and the gain settles toward &lt;code&gt;0.42&lt;/code&gt;. At steady state it says: &lt;em&gt;blend about 42% of each new reading with 58% of what I already believed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That settling curve &lt;strong&gt;is&lt;/strong&gt; the memory forming. A system with no history trusts the present entirely. A system with accumulated belief holds its ground and treats new data as a partial correction. The filter cuts the tracking error roughly in half versus the raw sensor, not by having a better sensor, but by &lt;em&gt;remembering&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same shape, three times
&lt;/h2&gt;

&lt;p&gt;Now the payoff. Hold that structure in your head (a belief carried forward, a new observation folded in, a gain deciding the mix) and look at the other two problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent memory
&lt;/h3&gt;

&lt;p&gt;An AI agent in a long conversation is running the same loop, informally. Its "state" is its working understanding of you: your goals, your preferences, the facts you've established. Each new message is a noisy measurement, noisy because a single message can be sarcastic, a typo, a one-off, or a genuine lasting signal.&lt;/p&gt;

&lt;p&gt;A naive agent that just embeds every message and retrieves by similarity has &lt;strong&gt;no gain&lt;/strong&gt;. It treats every observation as equally weighted, forever. That's why it surfaces a preference you abandoned months ago, or lets one stray message overwrite a stable fact. It has storage, not memory.&lt;/p&gt;

&lt;p&gt;A real memory system needs the equivalent of &lt;code&gt;K&lt;/code&gt;: a judgment about how much each new message should update the persistent picture versus how much accumulated history to preserve. Promote the durable, down-weight the transient, and, crucially, let uncertainty decay so old, unconfirmed beliefs loosen their grip. That's a Kalman gain in disguise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Video object permanence
&lt;/h3&gt;

&lt;p&gt;A video model watching that ball roll behind a wall has a hidden state too: &lt;em&gt;what objects exist in this scene and where.&lt;/em&gt; Each frame is a measurement. When the ball is occluded, there's no measurement for it, and this is exactly the predict step with no update. A model with real state &lt;strong&gt;carries the ball forward&lt;/strong&gt; through the occlusion (&lt;code&gt;x = F * x&lt;/code&gt;), holds its position with growing uncertainty (&lt;code&gt;P&lt;/code&gt; grows), and re-acquires it when it reappears.&lt;/p&gt;

&lt;p&gt;A model &lt;em&gt;without&lt;/em&gt; persistent state does the opposite: with no measurement, the ball ceases to exist, and when it reappears the model hallucinates a fresh object or invents a physics story to explain the "teleport." That failure mode is well documented in current video LLMs. It's not a perception failure. It's a &lt;em&gt;missing predict step&lt;/em&gt;, no memory to carry the object through the moment it couldn't be seen.&lt;/p&gt;

&lt;p&gt;Object-centric architectures (slot attention) and state-space models (Mamba and kin) are, in effect, learned generalizations of this same predict/update loop, one keeping a slot per object, the other keeping a compressed running state, both deciding per-step what to write and what to ignore. Mamba's "selective" update, where the model learns what to fold into state and what to skip, is the Kalman gain grown up and made learnable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the analogy breaks (the honest part)
&lt;/h2&gt;

&lt;p&gt;If I stopped here I'd be overselling it, so here's where the equivalence is real and where it's a useful loose metaphor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real:&lt;/strong&gt; the &lt;em&gt;structure&lt;/em&gt; is genuinely shared. Predict-then-update, a persistent belief with quantified uncertainty, and a gain that trades history against observation. That recurrence shows up in all three, and it's not a coincidence. It's what optimal recursive estimation looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loose:&lt;/strong&gt; the classic Kalman filter assumes &lt;strong&gt;linear dynamics and Gaussian noise.&lt;/strong&gt; Bearing wear is roughly fine with that. Conversations and video are not. The state is high-dimensional, the dynamics are nonlinear, the noise isn't Gaussian. LLMs and video models do &lt;strong&gt;not&lt;/strong&gt; literally compute a Kalman gain. They learn something functionally analogous through attention and gating, which is far more expressive and far less interpretable. The extended and unscented Kalman filters stretch the math toward nonlinearity, but nobody's running a literal EKF inside a transformer.&lt;/p&gt;

&lt;p&gt;So the claim isn't "these systems are Kalman filters." The claim is that the Kalman filter is the &lt;strong&gt;simplest, most transparent member of a family&lt;/strong&gt;, and studying it teaches you the question every member of that family must answer. When you understand &lt;em&gt;why&lt;/em&gt; the gain settles, you understand what agent memory and video permanence are reaching for, and why "just store everything and retrieve by similarity" was never going to be memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Memory is not storage. Storage is keeping everything. Memory is &lt;em&gt;judgment about what to keep&lt;/em&gt;: carrying a belief forward, weighing each new observation against it, and letting uncertainty decide the mix.&lt;/p&gt;

&lt;p&gt;The Kalman filter is the oldest clean instance of that judgment we have. It's sixty years old, it's twenty lines of Python, and it's quietly the same thing the newest agent-memory stacks and video world-models are trying to relearn at scale.&lt;/p&gt;

&lt;p&gt;If you build agents, the filter is worth an afternoon. Not because you'll implement one (you won't) but because it makes one question concrete and unforgettable: for every new thing your system sees, &lt;em&gt;what is it keeping, and what is it letting go?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, the same lens (hidden state, noisy observation, what to carry forward) is how I think about predictive maintenance on industrial machinery, where the "hidden state" is a machine's health and the "measurement" is its power signature. Different domain, identical question.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>computervision</category>
      <category>agents</category>
    </item>
    <item>
      <title>Stop Building MCP Wrappers — Build Domain-Specific Tools Instead</title>
      <dc:creator>Goutham Nishkal Deepu EDA</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:16:53 +0000</pubDate>
      <link>https://dev.to/goutham_nishkaldeepueda/stop-building-mcp-wrappers-build-domain-specific-tools-instead-49ci</link>
      <guid>https://dev.to/goutham_nishkaldeepueda/stop-building-mcp-wrappers-build-domain-specific-tools-instead-49ci</guid>
      <description>&lt;p&gt;A few weeks ago, Pinterest engineering published numbers from their production MCP deployment: ~66,000 monthly tool invocations, 844 active users, an estimated 7,000 engineering hours saved per month. (&lt;a href="https://chatforest.com/guides/mcp-ecosystem-2026-state-of-the-standard/" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;What stood out wasn't the scale. It was &lt;em&gt;what they didn't skip&lt;/em&gt; — every server passed security, legal, privacy, and compliance review before going to production. Sensitive operations required human approval.&lt;/p&gt;

&lt;p&gt;That's the part most MCP tutorials skip. Most guides show you how to wrap an API in five minutes. Almost none show you how to design a tool that's actually safe and useful for an AI agent to call autonomously.&lt;/p&gt;

&lt;p&gt;This post walks through that gap — using a generic incident/log-analysis domain as the example — with working FastMCP 3.x code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrapper instinct
&lt;/h2&gt;

&lt;p&gt;The fastest way to build an MCP server is to take an existing API and expose every endpoint as a tool, 1:1. It looks something like this:&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;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incidents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_incident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch a single incident by ID.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM incidents WHERE 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;incident_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_incidents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List all incidents.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM incidents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_incident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Update an incident&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s status.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE incidents SET status = ? WHERE 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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It'll pass a demo. It's also exactly the pattern that causes problems in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No domain semantics.&lt;/strong&gt; The model has to infer what "status" values are valid, what counts as a duplicate incident, what severity actually means in your system — none of that is encoded anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No governance.&lt;/strong&gt; &lt;code&gt;update_incident&lt;/code&gt; can be called with zero friction. An agent that misreads a request can silently change production state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context bloat.&lt;/strong&gt; One developer building on GitHub's MCP server described it as dumping over 40 tools into context before doing anything — which measurably degrades agent performance, since the model has to reason over every tool description on every turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A thin 1:1 wrapper is a fast way to get a demo. It is rarely the right shape for a tool an agent calls unsupervised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing a domain-specific tool instead
&lt;/h2&gt;

&lt;p&gt;The alternative isn't more tools — it's &lt;em&gt;fewer, smarter&lt;/em&gt; tools that encode the judgment a domain expert would apply by hand.&lt;/p&gt;

&lt;p&gt;Here's the same incident-analysis domain, redesigned:&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;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;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="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="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident-analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;LOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;MEDIUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;HIGH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CRITICAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IncidentSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;by_severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;likely_duplicates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incident IDs that may be duplicates based on time/error-signature clustering&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;needs_human_review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize_recent_incidents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;min_severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IncidentSummary&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 incidents from the last N hours at or above a severity threshold.

    Applies domain logic to flag likely duplicates (same error signature within
    a 10-minute window) rather than returning raw rows for the model to interpret.
    Returns needs_human_review=True if any CRITICAL incidents are unresolved,
    signaling the caller should not auto-close or auto-triage without a human.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM incidents WHERE created_at &amp;gt;= ? AND severity &amp;gt;= ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;duplicates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_cluster_by_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;has_unresolved_critical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&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="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&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;IncidentSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;by_severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_count_by_severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;likely_duplicates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;needs_human_review&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;has_unresolved_critical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;propose_incident_resolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Propose (but do not apply) a resolution for an incident based on
    similar past incidents and their resolutions.

    This tool is read-only by design. It never mutates incident state —
    use apply_resolution() separately, which requires explicit human
    confirmation, to actually close anything out.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;incident&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM incidents WHERE 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;incident_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;similar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_find_similar_resolved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident_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;incident_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;proposed_resolution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_summarize_resolution_pattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;based_on&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_resolution_confidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_resolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolution_note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirmed_by_human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Apply a resolution to an incident. Requires confirmed_by_human=True.

    This is the only tool in this server that mutates incident state.
    It exists separately from propose_incident_resolution() so that an
    agent can never go from &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; in a single unsupervised call.
    &lt;/span&gt;&lt;span class="sh"&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="n"&gt;confirmed_by_human&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apply_resolution requires confirmed_by_human=True. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use propose_incident_resolution() first and surface it to a person.&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE incidents SET status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resolved&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, resolution=? WHERE 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;resolution_note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incident_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things changed, deliberately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain logic moved into the tool, not the prompt.&lt;/strong&gt; "What counts as a likely duplicate" is a judgment call specific to this domain — clustering by error signature within a time window. Encoding it in the tool means every caller gets the same correct answer instead of the model re-deriving (or guessing at) the logic every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read and write are different tools.&lt;/strong&gt; &lt;code&gt;propose_incident_resolution&lt;/code&gt; can never mutate anything — it's structurally read-only. &lt;code&gt;apply_resolution&lt;/code&gt; is the single narrow path to a state change, and it refuses to run without an explicit human-confirmation flag. This is a simplified version of the human-in-the-loop pattern Pinterest's writeup describes for sensitive operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return types are structured, not raw rows.&lt;/strong&gt; The &lt;code&gt;IncidentSummary&lt;/code&gt; Pydantic model gives the agent (and you) a typed, predictable contract — and the &lt;code&gt;needs_human_review&lt;/code&gt; flag does some of the model's reasoning for it, rather than hoping it infers urgency correctly from a list of dicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three tools, not three-times-N.&lt;/strong&gt; Instead of exposing every table operation, this server exposes three tools that map to how a human on-call engineer actually thinks: "what's going on," "what would I do about it," "okay, do that." Fewer tools in context, each carrying more domain weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than the protocol choice
&lt;/h2&gt;

&lt;p&gt;MCP itself is becoming infrastructure — it went from an internal Anthropic spec to a Linux Foundation standard backed by OpenAI, Google, and Microsoft in about 16 months. (&lt;a href="https://chatforest.com/guides/mcp-ecosystem-2026-state-of-the-standard/" rel="noopener noreferrer"&gt;source&lt;/a&gt;) That convergence means the protocol layer is rapidly commoditizing. Every team will have access to the same transport, the same SDKs, the same client support.&lt;/p&gt;

&lt;p&gt;What won't commoditize is tool design. The difference between a server that's safe to hand to an autonomous agent and one that isn't comes down to exactly the choices above — what's read-only, what requires confirmation, what domain logic gets encoded once instead of re-derived every call.&lt;/p&gt;

&lt;p&gt;If you're building an MCP server right now, the protocol decisions are mostly made for you. The judgment calls aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start, if you want to try this yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add fastmcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# server.py — minimal runnable version
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident-analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ping&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Health check.&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;pong&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastmcp dev server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That spins up the MCP Inspector so you can see raw tool calls and responses before wiring it into Claude Desktop, Cursor, or any other MCP client.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a companion piece to a short series I've been writing on LinkedIn about what's actually missing in how organizations adopt AI — from training-time limitations to why "owning" your AI capabilities means doing the unglamorous design work, not just picking a model.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>python</category>
      <category>agenticai</category>
    </item>
  </channel>
</rss>
