<?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: MadeBy.Expert</title>
    <description>The latest articles on DEV Community by MadeBy.Expert (madebyexpert).</description>
    <link>https://dev.to/madebyexpert</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%2Forganization%2Fprofile_image%2F14987%2F15d990fc-116e-4df9-b163-40395dd41ee9.png</url>
      <title>DEV Community: MadeBy.Expert</title>
      <link>https://dev.to/madebyexpert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madebyexpert"/>
    <language>en</language>
    <item>
      <title>The reconciliation step where we use no model at all</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:42:31 +0000</pubDate>
      <link>https://dev.to/madebyexpert/the-reconciliation-step-where-we-use-no-model-at-all-5gp7</link>
      <guid>https://dev.to/madebyexpert/the-reconciliation-step-where-we-use-no-model-at-all-5gp7</guid>
      <description>&lt;p&gt;Our &lt;a href="https://madeby.expert/lead-demo.html" rel="noopener noreferrer"&gt;Lead Agent demo&lt;/a&gt; prepares a discovery brief for a fictional prospect. The prospect reconciles 3,000 subscriptions a week between HubSpot and Stripe, found 240 discrepancies last week and wants a read-only discrepancy report for an operations lead.&lt;/p&gt;

&lt;p&gt;A good brief prepares the first conversation. It does not solve the reconciliation. To make that boundary concrete, we built the reconciliation itself as a separate &lt;a href="https://madeby.expert/reconciliation-demo.html" rel="noopener noreferrer"&gt;checked example&lt;/a&gt;. It makes no model calls.&lt;/p&gt;

&lt;p&gt;Everything below uses a constructed dataset with known answers. There is no live HubSpot or Stripe connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the comparison contract first
&lt;/h2&gt;

&lt;p&gt;Most of the difficulty in reconciliation is deciding what "the same" means. The example states its contract explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rows join on one stable subscription ID, exactly, with no fuzzy matching;&lt;/li&gt;
&lt;li&gt;both sides must describe the same monthly period;&lt;/li&gt;
&lt;li&gt;amounts are EUR, as non-negative safe integer cents;&lt;/li&gt;
&lt;li&gt;statuses come from one agreed vocabulary: &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;paused&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;, &lt;code&gt;past_due&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are normalized comparison fields, not raw provider schemas. For a real system, someone has to decide whether an amount includes tax, discounts, proration and refunds, and how each tool's statuses map to the shared vocabulary. That decision belongs to the customer's finance or operations owner, not to a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every pair gets one outcome and its reasons
&lt;/h2&gt;

&lt;p&gt;The engine indexes both sides by subscription ID and gives every ID one of three outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Amount delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;match&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both rows valid, same amount and status&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discrepancy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Amount or status differs, or one side is missing&lt;/td&gt;
&lt;td&gt;Known, or &lt;code&gt;null&lt;/code&gt; when a side is missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate ID, period mismatch, unsupported currency, invalid amount, unknown status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core of the comparison is short. Simplified from the demo:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;amountDeltaCents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountCents&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountCents&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDeltaCents&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;amountDeltaCents&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Amount total exceeds safe integer range&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;amountDeltaCents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount_mismatch&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status_mismatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discrepancy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;match&lt;/span&gt;&lt;span class="dl"&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 choices matter here. A missing counterpart is flagged as a discrepancy, but its monetary value stays unknown: we do not invent the amount of a row that is not there. And if the running total would leave the safe integer range, the run stops instead of rounding.&lt;/p&gt;

&lt;p&gt;Each outcome keeps its source rows and reason codes, so a reviewer can see why a pair was flagged without re-running anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check against answers the engine never sees
&lt;/h2&gt;

&lt;p&gt;A reconciliation that reports 240 discrepancies looks right when you expected 240. That is not a test. We needed an independent answer for every row.&lt;/p&gt;

&lt;p&gt;The generator produces 3,000 CRM rows, 3,000 billing rows and a separate expected-result manifest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expected case&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matching subscriptions&lt;/td&gt;
&lt;td&gt;2,730&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount differences of +€5.00&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status differences&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsupported currency (USD), review&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The engine does not read the manifest. A verifier compares every pair afterwards: classification, reason codes and per-pair amount delta, plus the supported total.&lt;/p&gt;

&lt;p&gt;On this dataset, the run classifies all 3,000 pairs as expected. It finds all 240 planted discrepancies with no false positives and no missed cases, and every reason code matches. The supported amount delta is €900.00, with zero cents of error. The comparison itself runs in milliseconds; the build regenerates and re-verifies it each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers mean
&lt;/h2&gt;

&lt;p&gt;The €900 is an amount difference to investigate. It is not recovered revenue or a customer saving.&lt;/p&gt;

&lt;p&gt;It also does not cover everything. The 30 USD pairs remain unresolved in the review queue, excluded from the supported total, because the contract has no currency conversion rule. Converting them at some assumed rate would produce a tidier total and a less honest one.&lt;/p&gt;

&lt;p&gt;The public page publishes the summary, four source-linked examples, the 30-item review queue with pending decisions, and an evidence file with every input row, expected case and actual outcome. Anyone can check the classifications without our source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no model?
&lt;/h2&gt;

&lt;p&gt;Joining on an exact ID and subtracting integers are tasks where a model adds cost, latency and a new failure mode without adding judgment. We use models elsewhere in the same story: a decision model selects discovery context, and a generative model drafts the Lead brief. Neither is asked to certify financial arithmetic.&lt;/p&gt;

&lt;p&gt;Where a model might help later is in the work around the comparison: explaining a cluster of review cases, or drafting questions for the person who owns the status mapping. That would be a separate, measured addition, not a replacement for the deterministic check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and does not establish
&lt;/h2&gt;

&lt;p&gt;Zero errors here establishes that this implementation passes its own controlled check on a constructed dataset with declared rules. It does not establish production accuracy, robust entity matching, correct interpretation of a customer's rules, reviewer acceptance, reduced investigation time or savings.&lt;/p&gt;

&lt;p&gt;For a real pilot, the useful promise is a process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agree the comparison rules with the people who own them.&lt;/li&gt;
&lt;li&gt;Check the implementation against an approved reference set, including representative exceptions.&lt;/li&gt;
&lt;li&gt;Keep ambiguous cases visible instead of forcing them into a total.&lt;/li&gt;
&lt;li&gt;Measure how much investigation work remains for the team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only after those steps can anyone make a correctness or economic claim.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://madeby.expert/reconciliation-demo.html" rel="noopener noreferrer"&gt;inspect the reconciliation example&lt;/a&gt; and download its evidence. If your team reconciles records between two systems every week, a sample export and your current matching rules are a good starting point for a pilot conversation.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built an L1 ticket deflector that knows when to stop: LangGraph + human-in-the-loop</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 22:57:52 +0000</pubDate>
      <link>https://dev.to/madebyexpert/i-built-an-l1-ticket-deflector-that-knows-when-to-stop-langgraph-human-in-the-loop-4fd3</link>
      <guid>https://dev.to/madebyexpert/i-built-an-l1-ticket-deflector-that-knows-when-to-stop-langgraph-human-in-the-loop-4fd3</guid>
      <description>&lt;h1&gt;
  
  
  I built an L1 ticket deflector that knows when to stop: LangGraph + human-in-the-loop
&lt;/h1&gt;

&lt;p&gt;The same tickets arrive at every IT service desk: password resets, VPN that won't connect, "how do I install 7-Zip?", a printer that's offline again. A widely cited industry range puts routine requests at 50–70% of L1 volume — work a script could handle, except nobody trusts a script with the tickets that actually matter.&lt;/p&gt;

&lt;p&gt;Building an agent that answers everything is easy. Building one that knows when to hand a ticket to a human is not. I built the second kind and open-sourced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/zedxter/l1-ticket-deflector" rel="noopener noreferrer"&gt;github.com/zedxter/l1-ticket-deflector&lt;/a&gt; (MIT)&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that shapes everything
&lt;/h2&gt;

&lt;p&gt;Most "AI helpdesk" demos optimize for deflection rate. That's the wrong target. A high deflection rate is trivial if you let the model guess.&lt;/p&gt;

&lt;p&gt;The constraint I optimized for instead: never let the agent act on a sensitive request. Anything touching access, privileges, security, or money goes to a human with prepared context. The agent resolves the boring 30% and escalates the rest — with the relevant KB article already attached, so the human doesn't start from zero.&lt;/p&gt;

&lt;p&gt;I called it human-in-the-loop, but the honest framing is narrower: the agent has a hard boundary, and that boundary is the part worth testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph
&lt;/h2&gt;

&lt;p&gt;The production version is a LangGraph state machine. Three outcomes, no cleverness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;classify ──&amp;gt; no match / low confidence ──&amp;gt; queue (general L1 queue)
   │
   ├── sensitivity = low        ──&amp;gt; auto_resolve ──&amp;gt; notify_user
   └── sensitivity = high/crit  ──&amp;gt; human_review ──&amp;gt; escalate ──&amp;gt; notify_user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classifier returns a KB article ID and a confidence score. Two gates decide the path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confidence gate&lt;/strong&gt; — below &lt;code&gt;0.55&lt;/code&gt;, the ticket goes to the general queue. No guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitivity gate&lt;/strong&gt; — high/critical tickets go to &lt;code&gt;service_desk_l2&lt;/code&gt; or &lt;code&gt;security_ops&lt;/code&gt;, never to auto-resolve.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each KB article carries a &lt;code&gt;sensitivity&lt;/code&gt; label. That's the trick: the model doesn't decide what's dangerous. The knowledge base does, and it's a human-authored field. The LLM only picks the article.&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;route_after_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CONFIDENCE_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sensitivity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AUTO_RESOLVE_SENSITIVITY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto_resolve&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;human_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire safety story in six lines. Sensitive categories (access grants, license purchases, phishing reports, ransomware) can never reach &lt;code&gt;auto_resolve&lt;/code&gt;, no matter what the model says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two versions, on purpose
&lt;/h2&gt;

&lt;p&gt;The repo ships two implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;demo/&lt;/code&gt;&lt;/strong&gt; — a zero-dependency Python script. Keyword matching plus stemming, no LLM. It runs on a Raspberry Pi. Its job is to make the mechanics visible and the numbers checkable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/&lt;/code&gt;&lt;/strong&gt; — the LangGraph agent. RAG over the KB, structured output via Pydantic, tool-calls into the ITSM, &lt;code&gt;MOCK_MODE=1&lt;/code&gt; so you can run the logic without an API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kept the offline version because a demo you can't run is just a claim. Anyone can clone the repo and reproduce the numbers in ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers (and why I'm suspicious of them)
&lt;/h2&gt;

&lt;p&gt;On 45 labeled tickets, the offline classifier scored:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto-resolved&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalated to human&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No match → queue&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision accuracy&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing accuracy&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample deflection&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;100% accuracy is a red flag, not a trophy. It means the test set is too small and too clean. 45 tickets, hand-labeled, written by the same person who wrote the KB — of course it scores well. I'm publishing it because it's honest about what it is: a sanity check, not a benchmark. The real test is a client's messy backlog, and that number doesn't exist yet.&lt;/p&gt;

&lt;p&gt;The deflection rate is the one I'd defend. 55.6% on this sample, but the ROI model uses a conservative &lt;strong&gt;30%&lt;/strong&gt;, because market benchmarks for first-year deflection sit at 20–40% and I'd rather be wrong in the client's favor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ROI model
&lt;/h2&gt;

&lt;p&gt;Conservative assumptions for a ~500-person company in DACH:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1 tickets / month&lt;/td&gt;
&lt;td&gt;~1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deflection&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg handling time&lt;/td&gt;
&lt;td&gt;19 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IT rate (fully loaded)&lt;/td&gt;
&lt;td&gt;€45 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time saved&lt;/td&gt;
&lt;td&gt;~114 h/month (~0.7 FTE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Savings&lt;/td&gt;
&lt;td&gt;~€5,130 / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retainer&lt;/td&gt;
&lt;td&gt;€4,000 / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROI&lt;/td&gt;
&lt;td&gt;1.3× / month (15× / year)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The retainer is deliberately below the savings. A model where the vendor captures all the value is a model nobody signs. 1.3×/month is not a spectacular number — it's a defensible one, and defensible is what gets a pilot approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's fake and what's real
&lt;/h2&gt;

&lt;p&gt;I want to be precise about this, because "proof of work" usually means "screenshot of a demo."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real:&lt;/strong&gt; the classifier, the graph, the routing logic, the KB structure, the ROI math. Clone it, run it, read every line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stubbed:&lt;/strong&gt; the tool-calls. &lt;code&gt;itsm_create_ticket&lt;/code&gt;, &lt;code&gt;directory_reset_password&lt;/code&gt;, and &lt;code&gt;mdm_push_install&lt;/code&gt; return strings, not real ITSM actions. Wiring them to Jira Service Management or Zammad over MCP is a pilot task, not a demo task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not built:&lt;/strong&gt; the Slack/Teams approval UI, telemetry, multilingual KB. They're on the roadmap and I'm not pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building this
&lt;/h2&gt;

&lt;p&gt;Three things I'd carry into the next version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put the safety boundary in data, not in the prompt.&lt;/strong&gt; A &lt;code&gt;sensitivity&lt;/code&gt; field on each KB article is auditable. "The model is instructed to be careful" is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the runnable offline version.&lt;/strong&gt; It costs an afternoon and buys all the credibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the suspicious number.&lt;/strong&gt; 100% accuracy invites the right question — "on how many tickets?" — and answering it honestly is worth more than a flattering metric.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repo is MIT-licensed and the whole point is that you can pull it apart. If you run an IT desk and want to compare deflection on your own tickets, that's the interesting experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/zedxter/l1-ticket-deflector" rel="noopener noreferrer"&gt;github.com/zedxter/l1-ticket-deflector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build AI agents for reporting, CRM, and support workflows — with human review where it counts. Based in Potsdam, Germany.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langgraph</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>Agent Memory's Real Failure Is Currency, Not Retrieval</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:14:32 +0000</pubDate>
      <link>https://dev.to/madebyexpert/agent-memorys-real-failure-is-currency-not-retrieval-40h9</link>
      <guid>https://dev.to/madebyexpert/agent-memorys-real-failure-is-currency-not-retrieval-40h9</guid>
      <description>&lt;p&gt;Retrieval returns the correct record. The record is stale. And because the model states fresh and dead facts with identical confidence, nothing in the output tells you which one you got.&lt;/p&gt;

&lt;p&gt;Your agent's memory isn't broken because it can't find the fact. It's broken because the fact it found stopped being true.&lt;/p&gt;

&lt;h2&gt;
  
  
  That is not a retrieval problem
&lt;/h2&gt;

&lt;p&gt;It's a state-management problem — the same one distributed systems have always had: validity windows, invalidation, cache coherency. A memory layer with none of those is a cache that never expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;Memory is now a first-class layer with its own benchmarks. Mem0, for example, &lt;a href="https://mem0.ai/research" rel="noopener noreferrer"&gt;reports&lt;/a&gt; 92.5 on LoCoMo and 94.4 on LongMemEval for its managed platform, at about 6,900 tokens per retrieval call. These are the vendor's own figures as published on 13 October 2026; I have not reproduced them.&lt;/p&gt;

&lt;p&gt;High retrieval scores answer one question: can the system find the fact? They don't answer whether the fact it found is still true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stale facts get surfaced. They just don't get acted on.
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2605.06527" rel="noopener noreferrer"&gt;STALE benchmark&lt;/a&gt; (May 2026) tests exactly that gap: whether an agent notices that a later observation has invalidated something it stored earlier. The paper reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;for LightMem, the strongest memory-framework baseline in the comparison, the new evidence appeared in retrieval results for 77.5% of the state-resolution and premise-resistance cases, yet only 3.3% of the old entries it retrieved were judged as needing an update;&lt;/li&gt;
&lt;li&gt;on the simplest conflict type, Gemini 3.1 Pro scored 92.0% when asked directly whether the old belief still held, and 30.0% when the question presupposed the old state;&lt;/li&gt;
&lt;li&gt;even that strongest model reached only 55.2% overall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact wasn't missing. It was present, and discounted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that works
&lt;/h2&gt;

&lt;p&gt;Zep's &lt;a href="https://help.getzep.com/graphiti/getting-started/overview" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt; shows the shape of the fix: graph edges carry temporal metadata, contradicting facts are handled by invalidating the old edge instead of overwriting it, and queries can ask for the current state or a point in the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Memory is not "accumulate more." A fact needs a bounded window of validity — otherwise you're shipping an agent that is confidently wrong on a schedule.&lt;/p&gt;

&lt;p&gt;How are you handling staleness in your agent's memory layer?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>A weekly reporting workflow that stops when its sources are incomplete</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:18:03 +0000</pubDate>
      <link>https://dev.to/madebyexpert/a-weekly-reporting-workflow-that-stops-when-its-sources-are-incomplete-5abl</link>
      <guid>https://dev.to/madebyexpert/a-weekly-reporting-workflow-that-stops-when-its-sources-are-incomplete-5abl</guid>
      <description>&lt;p&gt;A weekly report can contain plausible numbers and still be unsuitable for a decision. A missing support export or an old CRM snapshot changes what the report can honestly say.&lt;/p&gt;

&lt;p&gt;I built a &lt;a href="https://madeby.expert/demo.html" rel="noopener noreferrer"&gt;small reporting demo for MadeBy.Expert&lt;/a&gt; to make those conditions visible. It uses synthetic billing, CRM and support snapshots, deterministic calculations and a template narrative. There are no live integrations or language model calls in this version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the numbers before automating the report
&lt;/h2&gt;

&lt;p&gt;The sample distinguishes monthly recurring revenue from revenue collected during the week. Open pipeline is the unweighted value of open opportunities, not a revenue forecast. Support metrics distinguish unresolved tickets at the snapshot from tickets resolved within the reporting period.&lt;/p&gt;

&lt;p&gt;Each metric includes a source reference and a definition. Those details give the reviewer something concrete to check against the input.&lt;/p&gt;

&lt;p&gt;For a client workflow, I would agree on these definitions before connecting the source systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A missing source is a report state
&lt;/h2&gt;

&lt;p&gt;The browser demo lets you switch between complete sources, missing support data and stale CRM data. The report keeps the metrics it can calculate, omits the unavailable ones and records a blocker. It does not substitute zero for a missing value.&lt;/p&gt;

&lt;p&gt;The final state comes from that blocker list:&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this example, all three sources are required for approval. A snapshot is stale if it was captured before the reporting period ended or more than 24 hours before the report's &lt;code&gt;asOf&lt;/code&gt; time. A future timestamp is rejected as invalid input.&lt;/p&gt;

&lt;p&gt;Those are choices for this workflow. A daily operations report might need a tighter freshness limit; another report might be useful with an explicitly optional source. The thresholds need agreement with the person using the result.&lt;/p&gt;

&lt;p&gt;Here is the actual test covering all three sources and both failure modes. It uses Node's built-in test runner and strict assertions. &lt;code&gt;fixture()&lt;/code&gt; loads the synthetic input afresh for each case:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing/stale sources are unavailable and block approval&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;const&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support&lt;/span&gt;&lt;span class="dl"&gt;"&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;const&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stale&lt;/span&gt;&lt;span class="dl"&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fixture&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="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;capturedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-12T08:00:00Z&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&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="nf"&gt;approveReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Demo reviewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sr"&gt;/Blocked/&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;The last assertion matters: reporting the problem in the output is insufficient if the approval function still accepts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval applies to one version of the report
&lt;/h2&gt;

&lt;p&gt;The implementation computes a SHA-256 digest of the report content, which includes a digest of the input. A reviewer supplies the report digest when approving a local export. The approval function recomputes it and checks that the report is still a draft without blockers:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;approveReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;report&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="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Approval digest does not match the current report&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Blocked reports cannot be approved&lt;/span&gt;&lt;span class="dl"&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;reviewer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\r\n]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A named reviewer is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&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;Here, &lt;code&gt;hash&lt;/code&gt; is SHA-256 over &lt;code&gt;JSON.stringify(value)&lt;/code&gt; and &lt;code&gt;fail&lt;/code&gt; throws an error. This is an excerpt from the local prototype, not a standalone approval service.&lt;/p&gt;

&lt;p&gt;Rebuilding the report after changing the input produces a different digest, so the previous digest cannot approve the new report. The tests also alter a calculated metric directly and check that approval rejects the modified content.&lt;/p&gt;

&lt;p&gt;There are limits to this approach. The digest is a consistency check, not a signature or proof of who reviewed the report. The reviewer name is supplied as text; there is no authentication or authorization layer. The JSON serialization is also specific to this implementation, not a canonical format for independent producers.&lt;/p&gt;

&lt;p&gt;A deployed approval service would need to establish the reviewer's identity and permissions, store the review event, and bind the delivery action to the approved version. Those parts are not implemented here.&lt;/p&gt;

&lt;p&gt;The browser page shows the data conditions and report. The local runnable example handles approval and export; neither sends a report automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI could fit later
&lt;/h2&gt;

&lt;p&gt;A language model might help draft commentary from validated facts. That is a possible extension, not an implemented feature of this demo. I would keep metric calculation, source checks and the approval boundary explicit regardless of how the narrative is produced.&lt;/p&gt;

&lt;p&gt;The current prototype makes no claim about client time savings, live connector reliability or model quality. Those need measurement against an actual workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://madeby.expert/demo.html" rel="noopener noreferrer"&gt;Try the demo&lt;/a&gt;, including the missing-source and stale-source cases.&lt;/p&gt;

&lt;p&gt;If your team assembles a similar report manually, &lt;a href="https://madeby.expert/#contact" rel="noopener noreferrer"&gt;describe the workflow&lt;/a&gt;: which tools supply the data, how often the report is needed, and who checks it. We can discuss whether a small paid pilot with clear success criteria fits.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Typed Contracts Between AI Agents: The Interface That Actually Breaks</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Tue, 15 Sep 2026 05:44:04 +0000</pubDate>
      <link>https://dev.to/madebyexpert/typed-contracts-between-ai-agents-the-interface-that-actually-breaks-547j</link>
      <guid>https://dev.to/madebyexpert/typed-contracts-between-ai-agents-the-interface-that-actually-breaks-547j</guid>
      <description>&lt;p&gt;Most multi-agent failures I've seen don't happen &lt;em&gt;inside&lt;/em&gt; an agent. They happen &lt;strong&gt;between&lt;/strong&gt; two of them.&lt;/p&gt;

&lt;p&gt;When the handoff between two agents is treated as loosely structured JSON or implicit shared state, the interface is underspecified — and underspecified interfaces fail quietly.&lt;/p&gt;

&lt;p&gt;A production handoff should be treated like any other API contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an explicit input schema;&lt;/li&gt;
&lt;li&gt;an explicit output schema;&lt;/li&gt;
&lt;li&gt;required and optional fields;&lt;/li&gt;
&lt;li&gt;semantic constraints, not only valid JSON;&lt;/li&gt;
&lt;li&gt;a clear rejection and fallback path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;A research agent should not hand an unstructured object to a scoring agent. It should emit something the scorer can actually validate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchResult&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;subject_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;evidence&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="n"&gt;EvidenceItem&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;min_length&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="n"&gt;source_count&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="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ge&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="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="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;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scorer runs only after that result passes validation. That gate is the important part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probabilistic output, consequential action
&lt;/h2&gt;

&lt;p&gt;An LLM output is probabilistic. A database write, an external message, or a business decision is an action. Those two steps should not be directly coupled.&lt;/p&gt;

&lt;p&gt;Between them, put a deterministic gate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the model output.&lt;/li&gt;
&lt;li&gt;Validate its schema and business rules.&lt;/li&gt;
&lt;li&gt;Reject, retry, route to review, or stop the branch when validation fails.&lt;/li&gt;
&lt;li&gt;Execute the action only on an accepted contract.&lt;/li&gt;
&lt;li&gt;Record the handoff version and validation result.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it changes failure behavior
&lt;/h2&gt;

&lt;p&gt;Without a contract, a missing field becomes a null value, the null becomes a misleading score, and the score becomes a bad CRM update. The symptom shows up several steps away from the cause, and by then the trail is cold.&lt;/p&gt;

&lt;p&gt;With contracts, the same failure is localized: &lt;code&gt;ResearchAgent -&amp;gt; ScoringAgent&lt;/code&gt;, field &lt;code&gt;source_count&lt;/code&gt;, validation rule failed.&lt;/p&gt;

&lt;p&gt;That is more than defensive programming. It is architecture for software systems whose internal steps are non-deterministic.&lt;/p&gt;

&lt;p&gt;It gives staff engineers independently testable boundaries, helps principal engineers reduce coupling between teams and agents, and gives architects and CTOs a clearer way to reason about compatibility, ownership, and recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every probabilistic step that feeds a consequential action needs an explicit validation gate. Typed contracts are one strong way to define that boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model can be creative inside the boundary. The boundary should not be.&lt;/p&gt;

&lt;p&gt;Where have you found the most valuable contract in an agent workflow — between agents, between an agent and a tool, or before the final action?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building AI Agents That Actually Work: What Nobody Tells You</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:11:40 +0000</pubDate>
      <link>https://dev.to/madebyexpert/building-ai-agents-that-actually-work-what-nobody-tells-you-2f88</link>
      <guid>https://dev.to/madebyexpert/building-ai-agents-that-actually-work-what-nobody-tells-you-2f88</guid>
      <description>&lt;p&gt;I've been building AI agents for a while now. Not chatbots. Not RAG demos. Real agents that take actions, make decisions, and run autonomously.&lt;/p&gt;

&lt;p&gt;Here's what nobody tells you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between demo and production
&lt;/h2&gt;

&lt;p&gt;Every AI agent framework shows you a 5-line demo that works perfectly. Then you deploy it, and it fails in ways you didn't imagine.&lt;/p&gt;

&lt;p&gt;The reason is simple: a demo is a happy path. Production is a graph of failure states.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Observability is not optional
&lt;/h2&gt;

&lt;p&gt;Your agent is only as good as your ability to see what it's doing.&lt;/p&gt;

&lt;p&gt;When an agent makes a wrong decision, you need to know exactly why. Was it a bad prompt? A hallucinated tool call? Missing context from a previous step?&lt;/p&gt;

&lt;p&gt;Log every thought. Audit every action. If you can't replay an agent's decision process, you can't trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Tools are the bottleneck, not the LLM
&lt;/h2&gt;

&lt;p&gt;Most people think the LLM is the hard part. It's not. The hard part is the tools.&lt;/p&gt;

&lt;p&gt;Your agent needs to call APIs, read databases, write files, send emails. Each of those is a failure point. Network timeout. Auth expired. Schema changed. Rate limited.&lt;/p&gt;

&lt;p&gt;Build your tool layer like you build a distributed system. Retries. Circuit breakers. Timeouts. Graceful degradation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The context window is a trap
&lt;/h2&gt;

&lt;p&gt;Long-running agents accumulate context. The more they do, the more context they carry. Eventually, the context window fills with noise, and the agent starts making bad decisions.&lt;/p&gt;

&lt;p&gt;Strategies that actually work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summarization&lt;/strong&gt; — compress old context into summaries, don't carry raw history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured memory&lt;/strong&gt; — separate short-term (current task) from long-term (learned patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting&lt;/strong&gt; — actively prune irrelevant context. If it didn't matter in the last 10 steps, it probably won't matter now&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Determinism is underrated
&lt;/h2&gt;

&lt;p&gt;Everyone wants creative agents. What you actually want is predictable agents.&lt;/p&gt;

&lt;p&gt;A creative agent that hallucinates a solution is useless. A predictable agent that follows a known pattern is valuable.&lt;/p&gt;

&lt;p&gt;Design for determinism first. Add creativity as a controlled parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The orchestration layer matters more than the model
&lt;/h2&gt;

&lt;p&gt;You can swap GPT-4 for Claude or Gemini and your agent still works — if your orchestration is solid.&lt;/p&gt;

&lt;p&gt;Good orchestration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear state machine&lt;/li&gt;
&lt;li&gt;Explicit error handling&lt;/li&gt;
&lt;li&gt;Human-in-the-loop for critical decisions&lt;/li&gt;
&lt;li&gt;Audit trail for every action&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Building AI agents that actually work is not an AI problem. It's a software engineering problem.&lt;/p&gt;

&lt;p&gt;The LLM is the easiest part. Everything around it — tools, observability, state management, error handling, orchestration — that's where the real work is.&lt;/p&gt;

&lt;p&gt;And that's also where the real value is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Your Agent Isn't Losing Memory. It's Rotting.</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Fri, 28 Aug 2026 18:33:00 +0000</pubDate>
      <link>https://dev.to/madebyexpert/your-agent-isnt-losing-memory-its-rotting-2edd</link>
      <guid>https://dev.to/madebyexpert/your-agent-isnt-losing-memory-its-rotting-2edd</guid>
      <description>&lt;p&gt;Agents don't forget because the model is bad. They forget because we built it into them. Six hours and forty messages in, what an agent needs is gone — or still there but wrong, drowned in noise.&lt;/p&gt;

&lt;p&gt;That's context rot.&lt;/p&gt;

&lt;p&gt;I've spent the last month inside a team of always-on AI agents that run for weeks. Different jobs, one codebase, one shared vault of lessons. They review each other's PRs, publish, plan. And they rot. I watched every flavor of it, then built the countermeasures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What context rot is
&lt;/h2&gt;

&lt;p&gt;Four failures that compound.&lt;/p&gt;

&lt;p&gt;Compression loss. When a session runs out of window, a summary replaces raw history. It sounds fine until the agent needs the detail it summarized away. I watched an agent repeat a plan that contradicted a decision two screens earlier. Nobody noticed, because the contradiction lived at a depth that was gone.&lt;/p&gt;

&lt;p&gt;Drift. Each turn pulls the agent toward the recent and the loud. Given ten pieces of context, it weights the last five. Over hundreds of turns it quietly solves a different problem than the one you assigned.&lt;/p&gt;

&lt;p&gt;Priority blurring. "We decided X" and "we looked at X" feel the same in the tokens. Decisions stop being binding and become suggestions.&lt;/p&gt;

&lt;p&gt;Noise accretion. Stale output and dead ends pile up. The signal-to-noise ratio falls, and a degraded ratio looks like a dumber model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;The cheap fix is "just enlarge the window." I built that reflex out of myself. Longer windows don't fix rot, they defer it — later in the timeline, larger cost per turn, and past some point the model does visibly worse than with a lean window. Remembering everything is a form of forgetting: the important thing drowns.&lt;/p&gt;

&lt;p&gt;And rot is per-profile, not per-agent. In a team sharing a codebase, what agent A rotted away still matters to agent B. So the fix must live outside any single session. Memory in one agent's head dies with that session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;None of this is exotic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Summarize to executable facts, not prose. Emit decisions and constraints as explicit, fixed-schema entries the agent can act on. A good summary answers "what is still binding" before "what happened."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move memory out of the session into a semantic index. Our biggest win. Keep lessons and decisions in a plain markdown vault, index with embeddings, and search returns meaning, not exact strings. "Offer-notification" and "pet-booking" are unrelated strings and near-identical problems; string search never connects them, the graph does — in milliseconds, on one SQLite file and a small local embedding model. No vector DB, no cluster, no bill. Don't ask one long-lived brain to do all the retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn knowledge into skills, not notes. A lesson read once and never referenced is a lesson that rots. The durable form is a skill: a procedure loaded on demand behind a trigger, pulled into context exactly when it applies and out the rest of the time. That's the difference between a memory of what to do and a memory of when to use it. The trigger is the part to get right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer memory by half-life. Instant preferences live in a compact always-on store; entity facts in a structured store you can query; long-lived conventions in a vault you load on purpose. Each level has its own audit cadence, so the always-on layer never bloats and the deep layer never goes stale silently. Cap and prune the always-on store on a schedule: an always-on memory that grows forever is context rot in slow motion.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Patterns to rot-proof an agent
&lt;/h2&gt;

&lt;p&gt;Whatever you pick, these hold.&lt;/p&gt;

&lt;p&gt;Make forgetting visible. If a session drops something, drop it loudly: what entered context, what left it, why. You can't manage decay you can't see.&lt;/p&gt;

&lt;p&gt;Bind decisions harder than facts. A decision needs a source, a status, an owner. A degraded decision is an incident, not a nuisance.&lt;/p&gt;

&lt;p&gt;Retrieve, don't carry. Pull the relevant slice on demand instead of carrying a fat context everywhere. The leaner the working set, the slower the rot.&lt;/p&gt;

&lt;p&gt;One source of truth for shared rules. When agents cooperate, conventions belong in one place anyone can read, not duplicated into private memory where they rot out of sync and you end up fighting three versions of the same rule.&lt;/p&gt;

&lt;p&gt;Treat memory as code. Schema changes, evals, tests. The vault is source, the index is a produced artifact: rebuild it on a cron and never hand-edit it. A knowledge graph you can't rebuild from source is a liability.&lt;/p&gt;

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

&lt;p&gt;You never beat context rot. You manage it. Design so the cost of rotting is contained and observable, and so the source of truth survives the session because it lives outside it.&lt;/p&gt;

&lt;p&gt;In our setup, week-long cycles now hold up better than the first version did at hour three. That is an observation from running them, not a measurement. The difference was never a better model. It was admitting that an agent's context is a perishable working surface, not a mind, and engineering it that way.&lt;/p&gt;

&lt;p&gt;I keep getting asked which framework solves this. The answer stays boring: write good compression, index for meaning, encode lessons as triggered skills, cap the always-on memory, and keep long-lived truth somewhere the agent can query and rebuild instead of carry. The agent will look cooler carrying everything. It will also rot faster.&lt;/p&gt;

&lt;p&gt;So: how do you decide what an agent is allowed to forget, and how do you make sure it tells you when it does?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>When Should You NOT Use an Agent?</title>
      <dc:creator>Danil Galeev</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:21:36 +0000</pubDate>
      <link>https://dev.to/madebyexpert/when-should-you-not-use-an-agent-4bk2</link>
      <guid>https://dev.to/madebyexpert/when-should-you-not-use-an-agent-4bk2</guid>
      <description>&lt;p&gt;Everyone is asking "should we use agents?" The real question is "when should we NOT?"&lt;/p&gt;

&lt;p&gt;I keep seeing teams bolt an agent on because it's the hot thing — then discover they reinvented a state machine with worse debugging. Agents don't solve a problem by existing. They are a mechanism for &lt;em&gt;deferring decisions to a runtime&lt;/em&gt;. When your inputs, tools, and failure modes are well-understood, that deferral buys you nothing but nondeterminism.&lt;/p&gt;

&lt;p&gt;The architecture question is not "LLM or not." It is: &lt;strong&gt;where does the judgment boundary sit?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places the boundary lands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A single agent is a program.&lt;/strong&gt; For a well-scoped task with a known toolset, you don't need a loop at all. You need a deterministic pipeline — with the LLM as one component, not the orchestrator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The moment you need a loop, you're building a runtime.&lt;/strong&gt; A runtime is a different beast. It has observability, tool permissions, credit and rate limits, and a way to explain what it did after the fact. That's not "more AI." That's distributed systems with a language model as the cognitive layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The expensive failure is capability you never signed up for.&lt;/strong&gt; Agents surface things you didn't design for: open-ended tool calls, emergent side-effects, scale that hits budgets or audit. This is where maturity shows — not in the cleverness of the model, but in the &lt;em&gt;constraints&lt;/em&gt; around it: entitlements, approval, observability, evals that cover the failure path, not just the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell
&lt;/h2&gt;

&lt;p&gt;Most of what teams call "agent architecture" is a decision-making boundary placed in the wrong spot, papered over with more layers. If you reach for a framework, an orchestrator, a runtime — stop and ask what you're actually deferring, and whether that deferral is working or just making the system harder to debug.&lt;/p&gt;

&lt;p&gt;A few heuristics I use before building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can the task be expressed as steps with a known order?&lt;/strong&gt; Pipeline with the LLM as a step — not an agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the agent invoke known tools with expected outputs?&lt;/strong&gt; A single governed agent, thin on top of a deterministic core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need to route mid-task to unexpected states?&lt;/strong&gt; Only then a real runtime, and only if you accept owning its observability, limits, and audit trail as first-class work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the failure non-recoverable?&lt;/strong&gt; Then don't put an agent in the loop at all. A wrong tool call inside a runtime can hurt you faster than a slower deterministic path ever will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Constrain before you automate. The agent looks cooler; it's also much harder to explain three months from now.&lt;/p&gt;

&lt;p&gt;So: when you're scoping a new system, what makes you reach for a deterministic pipeline instead of an agent — or the other way? I'd like the rules teams actually run with, not the ones they present in talks.&lt;/p&gt;

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