<?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: vsangaraju</title>
    <description>The latest articles on DEV Community by vsangaraju (@sangaraju1988).</description>
    <link>https://dev.to/sangaraju1988</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%2F4044809%2Fd01f98a0-8b0e-4b2f-a9c6-853ccbd3831c.jpg</url>
      <title>DEV Community: vsangaraju</title>
      <link>https://dev.to/sangaraju1988</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sangaraju1988"/>
    <language>en</language>
    <item>
      <title>The gate that stops an agent's memory from re-firing an attack</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Sat, 07 Nov 2026 04:37:47 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/the-gate-that-stops-an-agents-memory-from-re-firing-an-attack-1o8g</link>
      <guid>https://dev.to/sangaraju1988/the-gate-that-stops-an-agents-memory-from-re-firing-an-attack-1o8g</guid>
      <description>&lt;h2&gt;
  
  
  The carryover problem
&lt;/h2&gt;

&lt;p&gt;Most prompt-injection work treats the attack as a one-turn event. Agents with long-lived&lt;br&gt;
memory and scheduled tasks don't work like that. If the injection gets written into memory, or turns into a recurring&lt;br&gt;
task, it comes back. The attacker plants it once and leaves. The next session recalls it, the&lt;br&gt;
heartbeat fires it, and it runs again with no attacker present and no user in the loop.&lt;br&gt;
"After the Attacker Leaves" (CarryoverBench, arXiv:2610.28425) measures this. Planted payloads&lt;br&gt;
keep re-firing across rounds without decaying. That paper measures the attack. It doesn't&lt;br&gt;
build a defence. So I built one and piloted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gate does
&lt;/h2&gt;

&lt;p&gt;The idea is simple. Before a recalled memory entry or a scheduled task is allowed to execute,&lt;br&gt;
check where it came from. Not what it says. Where it came from.&lt;/p&gt;

&lt;p&gt;Every entry gets a derivation record when it's stored. The store writes it. The agent never&lt;br&gt;
does, because you can't trust an agent's later account of where something came from. The&lt;br&gt;
record holds the source channel, the author, parent links, a policy version, and a hash over&lt;br&gt;
the content and its parents' hashes. From that, the store assigns an authority class:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User-authored.&lt;/li&gt;
&lt;li&gt;From a source the user approved.&lt;/li&gt;
&lt;li&gt;Agent-derived, but only from class 1–3 parents with the chain intact.&lt;/li&gt;
&lt;li&gt;Untrusted or unknown: web and tool content, pasted third-party text, unknown imports, broken
chains, and anything the agent derived from class-4 material.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At re-fire time the gate checks the record again and applies a fixed table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classes 1–2 run.&lt;/li&gt;
&lt;li&gt;Class 3 can read freely. A side effect has to be inside a pre-approved mandate, otherwise the
gate asks the user.&lt;/li&gt;
&lt;li&gt;Class 4 can be read, with a label telling the agent not to follow instructions in it. If the
entry looks like an instruction, or it's about to cause a side effect, it's blocked and
quarantined.&lt;/li&gt;
&lt;li&gt;If the hash doesn't match or a parent is missing, it's quarantined as broken lineage, whatever
its class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The class-4 side-effect rule is block, not warn. That was a deliberate default.&lt;/p&gt;

&lt;p&gt;The gate is stdlib Python, deterministic, with no model calls. A check took tens of&lt;br&gt;
microseconds in the pilot (p50 37 µs in the harness, 53 µs on the benchmark).&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;There were two parts, and they measure different things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A synthetic carryover harness.&lt;/strong&gt; This is a small agent loop with a persistent memory file and&lt;br&gt;
a scheduled-task list. I planted five latent payloads in memory and five autonomous payloads as&lt;br&gt;
scheduled tasks. The autonomous ones include tasks created by an unknown plugin, tasks tampered&lt;br&gt;
on disk after storage, and tasks the agent built from pasted third-party text. Alongside them&lt;br&gt;
are legitimate controls (user reminders, agent summaries with intact chains, scheduled tasks&lt;br&gt;
the user created), some benign untrusted entries, and a no-injection scenario. Each scenario&lt;br&gt;
ran five rounds, gate off and gate on. I used two agents: a deterministic stub that obeys&lt;br&gt;
anything it can see, and qwen3-4b-instruct through Ollama.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon's Task-Tampering benchmark&lt;/strong&gt; (arXiv:2611.07640). I plugged the gate in as a defense&lt;br&gt;
condition in the benchmark's own runner. With the gate off, it's their runner untouched. The&lt;br&gt;
pilot used 30 of the 90 cases: the original injection for each, plus a clean run, gate off and&lt;br&gt;
on. That's 120 samples, and all 120 were scored.&lt;/p&gt;

&lt;p&gt;I wrote the hypotheses down and committed them before the pilot ran. Nothing in the gate was&lt;br&gt;
re-tuned after I saw numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Derivation-authority gating eliminates scheduled re-fires: on the synthetic harness, autonomous&lt;br&gt;
attacks went from 60% to 0% (ollama agent) and 100% to 0% (deterministic stub), with zero false&lt;br&gt;
positives on legitimate controls (0/605). On the single-session Task-Tampering benchmark the&lt;br&gt;
drop was 20% to 13.3% (6 to 4 of 30 cases), within noise at this size. The gate does not&lt;br&gt;
adjudicate truth: it withheld only 5 of 24 injection-bearing leaves, because most of this&lt;br&gt;
benchmark's injections read as plain facts.&lt;/p&gt;

&lt;p&gt;A bit more detail on each side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Harness.&lt;/strong&gt; With the real model, the autonomous channel went from 15 of 25 attack sessions to&lt;br&gt;
0 of 25. The planted tasks that fired every round with the gate off (the unknown plugin's task,&lt;br&gt;
the tampered task, the one built from pasted text) never ran with it on. Legitimate scheduled&lt;br&gt;
tasks finished at the same rate, 97%, in both conditions.&lt;/p&gt;

&lt;p&gt;The latent channel was flat at 20% for the real model, and that number needs explaining. The&lt;br&gt;
small model ignored four of the five planted memory notes even with the gate off. The one that&lt;br&gt;
worked was L5, a false claim that a landlord's account number had changed. It isn't phrased as&lt;br&gt;
an instruction, and I said before the run that the gate would miss it. It did, 5 of 5 rounds&lt;br&gt;
in both conditions. The stub, which obeys everything it sees, shows the mechanism more&lt;br&gt;
clearly: latent went from 100% to 20%, and L5 is the 20%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark.&lt;/strong&gt; Overall attack success went from 6 of 30 to 4 of 30. By environment, devops went&lt;br&gt;
from 3 of 10 to 1 of 10, marketplace was flat at 3 of 10, and productivity was 0 in both&lt;br&gt;
conditions. That's a two-sample swing on 30 cases, so I'm not claiming much from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;The biggest limit is in the title of that last result. The gate doesn't judge truth. It&lt;br&gt;
judges where something came from and whether it's trying to act. Most task-tampering injections&lt;br&gt;
in this benchmark don't say "ignore your instructions." They say things like "the policy was&lt;br&gt;
updated" or "this key needs write access." Those read as facts, so the gate shows them to the&lt;br&gt;
agent with an untrusted-origin label and moves on. Whether the agent believes them is up to the&lt;br&gt;
agent. That's a design limit, not something tuning would fix.&lt;/p&gt;

&lt;p&gt;It also isn't free on unlabelled content. In the clean benchmark runs, 13 of 30 samples (43.3%)&lt;br&gt;
had some gate intervention, either a withheld leaf or a call held for approval. In the&lt;br&gt;
harness, a third of the benign untrusted entries were quarantined. That third is one benign&lt;br&gt;
note written like a request, blocked every round. The 0% false-positive rate is on the&lt;br&gt;
legitimate controls as I defined them. It doesn't cover everything a real deployment would&lt;br&gt;
read.&lt;/p&gt;

&lt;p&gt;Two of my four hypotheses didn't come out cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;H3&lt;/strong&gt;, that the gate adds the most where the benchmark's own defenses are weakest, wasn't
tested. I ran the gate alongside the &lt;code&gt;none&lt;/code&gt; defense only. Composing it with spotlight,
sandwich, trust-hierarchy and DRIFT is the next run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H4&lt;/strong&gt;, the no-injection negative control, didn't hold. In the harness, the real model's
actions were the same with the gate on and off, but the email text wasn't. The gate had
withheld a benign note and added labels, and the model wrote the agenda differently. On the
benchmark, 20 of 30 clean cases matched. With one run per condition I can't separate gate
effects from run-to-run variance there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also hit a bug: the chain check treated a shared ancestor as a cycle, which mislabelled four&lt;br&gt;
blocked calls in the logs. None of them executed either way, so no outcome changed. It's fixed,&lt;br&gt;
with a regression test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits on the numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;30 of 90 benchmark cases, one injection variant each. A difference of a few samples is noise.&lt;/li&gt;
&lt;li&gt;The benchmark's LLM judge was mistral-nemo-12b running locally, not the model the paper used.
Its accuracy on my preflight probes was 2 of 3. The agent was a 4B open-weight model. So
none of my attack success rates compare to the paper's published numbers, and I'm not making
that comparison.&lt;/li&gt;
&lt;li&gt;The benchmark is single-session. I applied the gate where tools read content and where they
get called. Real cross-session re-firing is only exercised by the synthetic harness.&lt;/li&gt;
&lt;li&gt;In the benchmark adapter, the pre-approved mandate for agent-derived side effects is each
case's list of required tools. That stands in for a user who approved exactly what their task
needs. A real deployment would have to get that from the user.&lt;/li&gt;
&lt;li&gt;Running the benchmark locally took three logged deviations: a bigger context window, a
different judge (the first returned empty answers), and a request timeout. None touched the
gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So what does it show?
&lt;/h2&gt;

&lt;p&gt;Narrowly: if an attack only works by re-firing something the user never authorized, a scheduled&lt;br&gt;
task or a tampered entry or a summary laundered from web content, a deterministic provenance&lt;br&gt;
check at re-fire time shuts it down. In this pilot it did that with no false positives on&lt;br&gt;
controls and microsecond overhead. It is not a general defence against task-tampering, which&lt;br&gt;
in this benchmark mostly shows up as false facts the gate can't judge.&lt;/p&gt;

&lt;p&gt;The code, the preregistration, the full report and every run log are here:&lt;br&gt;
&lt;a href="https://github.com/sangaraju1988/carryovergate" rel="noopener noreferrer"&gt;https://github.com/sangaraju1988/carryovergate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you work on agent memory or scheduled agents, it's worth a look.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>BI MCP or Warehouse MCP? A 7-toggle rubric for picking the right doorway</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:04:37 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/bi-mcp-or-warehouse-mcp-a-7-toggle-rubric-for-picking-the-right-doorway-f3</link>
      <guid>https://dev.to/sangaraju1988/bi-mcp-or-warehouse-mcp-a-7-toggle-rubric-for-picking-the-right-doorway-f3</guid>
      <description>&lt;p&gt;One question I keep getting as a BI MCP builder serving a large enterprise user base: &lt;em&gt;which MCP should I use for this use case?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not "which one is better." Which doorway for &lt;em&gt;this&lt;/em&gt; job.&lt;/p&gt;

&lt;p&gt;The question usually arrives in a stronger form: "Why do we need a BI MCP when the warehouse MCP does the job?" The lazy answer is "because BI has a semantic layer." That's true and useless. It doesn't tell a team which doorway to give their agent on Monday.&lt;/p&gt;

&lt;p&gt;I got tired of answering it from scratch every time, so I turned the answer into seven toggles. Here they are — and there's an interactive version at the end that gives you the verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Who's asking?&lt;/strong&gt;&lt;br&gt;
Others — stakeholders, customers, anyone non-technical → BI MCP. They can't validate raw SQL; they need answers built from vetted definitions.&lt;br&gt;
You, and you know the data → warehouse MCP. A data engineer asking for themselves can read the SQL, spot a bad join, re-ask. No curation needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Does it need a defined business metric?&lt;/strong&gt;&lt;br&gt;
Yes — "revenue," "churn," "active user" → BI MCP. If two people can define the term differently, the definition must come from the model, not the model's imagination.&lt;br&gt;
No — row counts, null rates, "what's in this table" → warehouse MCP. No business definition at stake; raw SQL is the honest tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. One-off or repeated?&lt;/strong&gt;&lt;br&gt;
Repeated — dashboards, weekly reports → BI MCP. Repeated questions deserve a curated path: same definition, same shape, every time.&lt;br&gt;
One-off — debugging, "let me check something" → warehouse MCP. Ad-hoc work dies in curation queues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Do the consumers have warehouse access?&lt;/strong&gt;&lt;br&gt;
No → BI MCP, and this one is decisive. It's the only doorway that works. The BI tool queries under its own service identity while the end user has zero warehouse access; authorization happens at the BI project level. This is not a security hole — it's how BI works. The semantic layer is the authorization boundary.&lt;br&gt;
Yes → either. Through the warehouse MCP the agent acts with the requester's own grants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Exploring or asking a known question?&lt;/strong&gt;&lt;br&gt;
Exploring a new table, "what's possible here" → warehouse MCP. The curated model can't answer questions it wasn't built for.&lt;br&gt;
Known business question → BI MCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Does the agent need to write or create?&lt;/strong&gt;&lt;br&gt;
Yes — DDL, staging tables, "build me a mart" → warehouse MCP. Decisive. The BI doorway is read-only by design.&lt;br&gt;
No → either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Does the semantic model cover this?&lt;/strong&gt;&lt;br&gt;
No → warehouse MCP, or go model it first. The honest caveat the semantic-layer pitch skips: BI MCP is only as good as the model behind it. An unmodeled question through the BI doorway gets a confident wrong answer or no answer.&lt;/p&gt;

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

&lt;p&gt;Two of these are decisive: no warehouse access → BI MCP; needs writes → warehouse MCP. Otherwise, majority wins. A tie means hybrid — explore in the warehouse MCP, operationalize the metric in the semantic layer, serve it through BI MCP.&lt;/p&gt;

&lt;p&gt;"Warehouse MCP" covers BigQuery, Snowflake, Databricks, Fabric. "BI MCP" covers Looker, dbt, Cube — any semantic-layer doorway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I built the rubric as an interactive page — seven toggles, live verdict:&lt;br&gt;
&lt;a href="https://sangaraju1988.github.io/mcp-doorway-rubric/" rel="noopener noreferrer"&gt;https://sangaraju1988.github.io/mcp-doorway-rubric/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the repo (CC BY 4.0 — use it, credit it):&lt;br&gt;
&lt;a href="https://github.com/sangaraju1988/mcp-doorway-rubric" rel="noopener noreferrer"&gt;https://github.com/sangaraju1988/mcp-doorway-rubric&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two caveats
&lt;/h2&gt;

&lt;p&gt;Managed warehouse MCP endpoints ship with guardrails — row caps, runtime caps. They rarely bind for aggregated Q&amp;amp;A, but bulk extraction belongs to the warehouse API, not any MCP doorway.&lt;/p&gt;

&lt;p&gt;And unguarded agent + raw warehouse = surprise bills. Cost guardrails (quotas, dry runs, billed-bytes limits) belong in the warehouse MCP setup.&lt;/p&gt;

&lt;p&gt;Most mature teams end up running both doorways. The rubric just tells you which one for &lt;em&gt;this&lt;/em&gt; job.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>llm</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Data apps, defined: levels, principles, and a scorecard builders can actually use</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:06:17 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/data-apps-defined-levels-principles-and-a-scorecard-builders-can-actually-use-h8p</link>
      <guid>https://dev.to/sangaraju1988/data-apps-defined-levels-principles-and-a-scorecard-builders-can-actually-use-h8p</guid>
      <description>&lt;p&gt;&lt;em&gt;The Data App Framework, v2.0 · October 2026&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Venkata M Sangaraju, Sr. Database Engineer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On October 8, Anthropic put Claude Dashboards into beta. You connect Redshift, BigQuery, ClickHouse, Databricks or Snowflake, ask a question in plain English, and Claude writes the queries, picks the charts, and keeps the dashboard current. Four days later, Google announced Looker Data Apps in preview: an AI coding agent generates the app, and you publish it into Looker, where every query runs under the viewer's Looker permissions. Three weeks before either, Tableau ran its Dreamforce keynote under the title "Beyond BI" and introduced Tableau Data Apps, which you vibe-code with an outside agent and publish onto Tableau's governed data. Sigma's homepage now calls it a platform for "analytics, apps, and agents." Databricks has Apps. Snowflake has Streamlit and Native Apps. Domo has been selling "apps" for a decade and must be enjoying this.&lt;/p&gt;

&lt;p&gt;So everyone has data apps, and nobody agrees on what one is.&lt;/p&gt;

&lt;p&gt;I work on governed conversational analytics, and I keep having the same argument in design reviews. Someone shows an impressive demo, and the questions that decide whether it survives production get pushed to phase two: whose permissions does it run under, where do its metric definitions live, what happens when it writes something back. Those questions are the product, not a later phase.&lt;/p&gt;

&lt;p&gt;Platform teams are hearing a version of this a lot right now. Someone starts building in Claude Dashboards and likes what they get. Then they notice that their row-level and column-level security, and the role mappings behind it, already live in Looker or Tableau, and none of it carries over to the new tool. Nobody wants to rebuild a security model for every new tool. They want the new app to inherit the one they already trust. That's a reasonable ask, and today it's harder than it should be.&lt;/p&gt;

&lt;p&gt;This framework is my attempt to write down the vocabulary those reviews need: a definition, levels, principles, the failure modes I keep seeing, and a scorecard. Map your product against it and tell me where it's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a data app is
&lt;/h2&gt;

&lt;p&gt;A data app is a software application whose primary interface is live data, built on governed definitions, whose output is a decision or an action and not only a display.&lt;/p&gt;

&lt;p&gt;Live data rules out the static extract dressed up as an app. Governed definitions rules out the app that quietly re-implements "revenue" in its own SQL and drifts from the official number for a year. And decision or action rules out most of what got announced this year. If nothing can change because the app exists, it's a report with better marketing.&lt;/p&gt;

&lt;p&gt;Under this definition a dashboard isn't a different species from a data app. It's the simplest kind. What separates products is how much they're trusted to do, and in production systems trust should come in levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomy levels
&lt;/h2&gt;

&lt;p&gt;Classify a data app by the most consequential thing it is allowed to do, the way self-driving got classified by how much of the driving the machine does. Governance follows the level, not the vendor or the roadmap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What the app may do&lt;/th&gt;
&lt;th&gt;Governance floor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L0 View&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shows governed metrics; filtering is the whole interaction&lt;/td&gt;
&lt;td&gt;Certified definitions, viewer-level access control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L1 Explore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drill-downs, follow-up questions, conversational querying&lt;/td&gt;
&lt;td&gt;Row/column security enforced when the query runs, not in the UI; queries logged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2 Simulate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What-if inputs and scenarios; nothing is written anywhere&lt;/td&gt;
&lt;td&gt;Assumptions stored with every output, so a scenario can be re-run and audited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L3 Act&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes back to the warehouse or triggers a workflow; a human approves each action&lt;/td&gt;
&lt;td&gt;Runs as the end user; every write approved, logged and reversible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L4 Delegate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An agent acts on its own inside a pre-approved mandate&lt;/td&gt;
&lt;td&gt;Machine-readable mandate with scope, limits and expiry; a kill switch; lineage on every action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two clarifications, because this is where marketing will try to blur things. Level is per capability, not per app: an app that views at L0 and submits at L3 is an L3 app for governance purposes. And levels can't be skipped. An app that writes back while its row-level security lives only in the UI is missing a level, and that gap is where incidents come from.&lt;/p&gt;

&lt;p&gt;Now place the market. The BI vendors are climbing from the bottom. They're strong on L0–L1 governance and are adding write-back and actions: Sigma writes back to the warehouse and runs actions inside warehouse permissions, and Tableau's MCP servers push governed answers into Slack, Teams, Claude and ChatGPT, with published Data Apps inheriting Tableau's permissions.&lt;/p&gt;

&lt;p&gt;The AI labs are coming down from the top, with generation nobody can match and governance still arriving. Claude Dashboards shows the query behind every chart and when each chart last refreshed, a level of query transparency BI tools don't usually put in front of business users. Anthropic also positions it as working alongside BI tools for quick, exploratory questions, with a hand-off to Sigma, Hex, Omni and others when a question needs deeper analysis (Looker and Tableau are listed as coming soon). In this framework's terms, that's an L1 product that sends you to the governed layer when the stakes go up. It's an honest description of where each side is strong today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four principles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Definitions before displays.&lt;/strong&gt; A metric gets defined once, in the semantic layer, and every app consumes that definition. Once an app embeds its own calculation you have two sources of truth, and eventually a meeting about why they disagree. Looker built its product around this idea with LookML more than a decade ago, and it's still the clearest example of a metric defined once and reused everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Run as the user, not as the app.&lt;/strong&gt; Queries and actions execute with the requester's identity and permissions, down to the data and back up to whatever the app triggers. That can happen in two ways. The warehouse can see the user directly, through per-user credentials. Or a governed semantic layer can compile that user's row and column policy into every query it generates, on every surface, even though the warehouse sees one connection. Looker's access filters and Tableau's data policies work the second way, and it counts. What doesn't count is an app that connects to the warehouse with its own credentials and enforces permissions only in its interface. That app has stepped around the governed layer, and every user of it effectively holds the app's access. Pull up your warehouse query log and check where the app's queries come from. If it's a service account that isn't your governed semantic layer, your row-level security is a rumor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Writes are transactions, and derived data keeps its lineage.&lt;/strong&gt; A write-back needs the acting identity, the data it was based on, its approval state, and a way to reverse it. Whatever the app writes becomes someone else's source data tomorrow, so it should carry its lineage with it. When a number is wrong in a quarter-end review, "the app did it" isn't an answer. You need to know which app ran, as whom, on what data, and who approved it, and you can only answer that if the platform recorded the derivation at write time. I make the formal case for derivation-aware access control in agent systems in "Lineage-Aware Memory Governance" (IEEE Access, 2026). The short version is that access decisions have to know where data came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generated apps are drafts, and drafts need owners.&lt;/strong&gt; A prompt-generated app is unreviewed code whose author happens to be a model. Treat it like unreviewed code from a new hire: fine for exploration, gated before production, with the gate proportional to the autonomy level. Light for L0, mandatory for L3. Anthropic ships Dashboards switched off by default for Enterprise organizations, so an admin has to turn it on, and that's the right default for this category. Every app that does reach production also needs a named owner, a review date, and a way to be retired. BI teams spent a decade producing dashboard sprawl, thousands of unowned near-duplicates. Generated apps can produce the same sprawl in a quarter, and these ones hold credentials and can write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where governance has to live
&lt;/h2&gt;

&lt;p&gt;Every platform here is some arrangement of the same parts: sources, a data platform with its own security model, a governance layer (definitions, identity, policy, lineage), an app runtime, and the surfaces where people meet the app, from portals and embeds to chat tools and AI assistants. The architectural question that matters is whether the governance layer is a plane every runtime and surface passes through, or a feature each app implements for itself.&lt;/p&gt;

&lt;p&gt;Feature-level governance breaks down once the app is distributed. MCP is about to put the same app inside your portal, your Slack, your CRM and two AI assistants. If policy lives in the app, you maintain it in five places and the weakest copy defines your actual security. If it lives in the plane, enforced when the query executes against definitions written once, a new surface inherits it. Sigma argues for this with its warehouse-native design, which runs live queries with governance at the source. Tableau argues for it with Tableau Knowledge and its auto-built knowledge graph. They put it at different layers, but the instinct is the same, and I think it's correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes
&lt;/h2&gt;

&lt;p&gt;These are the ones I see most often. Name them in reviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The service-account masquerade.&lt;/strong&gt; The app queries the warehouse with its own service account, outside the governed layer, and per-user security exists only in its UI. Check the warehouse log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forked definitions.&lt;/strong&gt; The app computes its own version of a governed metric. To test, change the certified definition. If the app's number doesn't move, it's a fork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale data that looks live.&lt;/strong&gt; A "live" badge on a dashboard fed by an hourly pipeline, refreshing at per-click warehouse prices. Or an upstream schema change makes the app's queries fail quietly, and the first person to notice is an executive quoting an old number. Ask for the refresh contract per widget, and ask which contract tests catch a schema change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unreviewed promotion.&lt;/strong&gt; An app built for exploration picks up a submit button along the way, and nobody reclassifies or re-reviews it. There is probably an L3 capability in your estate right now that no one remembers approving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scorecard
&lt;/h2&gt;

&lt;p&gt;For vendor evaluations and release gates. Mark each question Described, Partially described, or Unknown — anything the platform's public material does not describe is Unknown, never absent. An app that fails question 2 or 4 shouldn't run above L1.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do the metrics resolve to the governed semantic layer, or does the app define its own?&lt;/li&gt;
&lt;li&gt;Does every query and action run under the end user's identity, either directly or through a governed layer that enforces that user's policy?&lt;/li&gt;
&lt;li&gt;Is row/column policy enforced at execution time on every surface where the app appears?&lt;/li&gt;
&lt;li&gt;Can every output be traced to its inputs, definitions and derivations?&lt;/li&gt;
&lt;li&gt;Can a reviewer see the generated SQL and logic before release?&lt;/li&gt;
&lt;li&gt;Is the app in a registry, with an owner and a review date?&lt;/li&gt;
&lt;li&gt;Is freshness a stated contract, and do contract tests catch schema or definition changes before users do?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using this
&lt;/h2&gt;

&lt;p&gt;The canonical copy of this framework lives at &lt;a href="https://github.com/sangaraju1988/data-app-framework" rel="noopener noreferrer"&gt;github.com/sangaraju1988/data-app-framework&lt;/a&gt;, versioned, with issues open. Cite and fork that copy and file corrections there; this post is the announcement. The first product mapping, for Claude Dashboards, is already in the repo. Vendors, publish your mapping against the levels and the scorecard. Builders, classify what you're shipping before your security team does it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Venkata M Sangaraju is a Sr. Database Engineer working on governed conversational analytics and agent memory governance, and the author of "Lineage-Aware Memory Governance" (IEEE Access, 2026).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>analytics</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your agent's skills have no lockfile — a derivation graph for skill provenance</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Sat, 24 Oct 2026 20:01:53 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/your-agents-skills-have-no-lockfile-a-derivation-graph-for-skill-provenance-2di3</link>
      <guid>https://dev.to/sangaraju1988/your-agents-skills-have-no-lockfile-a-derivation-graph-for-skill-provenance-2di3</guid>
      <description>&lt;p&gt;&lt;em&gt;Venkata M Sangaraju, Sr. Database Engineer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Repo: &lt;a href="https://github.com/sangaraju1988/skill-derivation-graph" rel="noopener noreferrer"&gt;github.com/sangaraju1988/skill-derivation-graph&lt;/a&gt; — spec v0.1, reference implementation, Zenodo DOI &lt;a href="https://doi.org/10.5281/zenodo.23623206" rel="noopener noreferrer"&gt;10.5281/zenodo.23623206&lt;/a&gt;. Discussion: &lt;a href="https://github.com/agentskills/agentskills/discussions/620" rel="noopener noreferrer"&gt;agentskills/agentskills#620&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A skill written for one business unit's reporting — how ambiguous cases should be handled — was copied by another team and built on. The original was later updated because some of the metrics it referenced had been deprecated. The copy never got that change. The other team's agent kept failing tool calls on the deprecated metrics, retried, tried to work around them on its own, and eventually errored out. It took a deep investigation to trace the failures back to a stale copy.&lt;/p&gt;

&lt;p&gt;One line saying "derived from X at ref Y" would have turned that into a quick lookup.&lt;/p&gt;

&lt;p&gt;That is the whole problem. Agent skills spread by copying, and a copy carries no record of where it came from. There is no lockfile for skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scale of the copy problem
&lt;/h2&gt;

&lt;p&gt;"Skill Constellations" (Seddik, UBC Okanagan, 8 October 2026, &lt;a href="https://arxiv.org/abs/2610.11169" rel="noopener noreferrer"&gt;arXiv:2610.11169&lt;/a&gt;) reconstructed a dated copy network over 2,193,119 skill adoptions on GitHub. Half of all SKILL.md files are verbatim copies of another file. And when a source skill is fixed, only 11.1% of changes to a clone group reach every copy. Most copies keep running the old version.&lt;/p&gt;

&lt;p&gt;So the ecosystem has the copy behaviour of a software supply chain and none of its provenance. A patched skill leaves most of its descendants silently running the vulnerable version, and nobody loading a skill can cheaply ask: is this current, and what is it derived from?&lt;/p&gt;

&lt;p&gt;Anthropic donated Agent Skills to the Agentic AI Foundation (AAIF, Linux Foundation) on 22 October 2026, and the starting roadmap covers versioned releases, a formal proposal process, and security guidance. Versioning answers "which release is this?" It does not answer "which skills descend from this one, and are they still carrying the old version?" That second question is what this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proposal: two optional keys
&lt;/h2&gt;

&lt;p&gt;The full spec is in the repo — &lt;a href="https://github.com/sangaraju1988/skill-derivation-graph/blob/master/SPEC.md" rel="noopener noreferrer"&gt;Skill Derivation Graph, v0.1&lt;/a&gt; — but the part I'm proposing to the AAIF Agent Skills repo is deliberately small: two optional string keys under the existing &lt;code&gt;metadata&lt;/code&gt; map in SKILL.md.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;derived-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/acme/skills/tree/3f2c9e1/doc-summarizer"&lt;/span&gt;
  &lt;span class="na"&gt;derived-from-hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256:9b1d…"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;derived-from&lt;/code&gt; is where this skill was copied or adapted from, pinned to a ref (commit SHA, tag, whatever the author uses). No version format is imposed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;derived-from-hash&lt;/code&gt; is the SHA-256 of the parent's normalised SKILL.md at that ref (line endings normalised to LF, trailing whitespace stripped, these two keys excluded).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verbatim vs. modified is not declared anywhere. Tooling derives it: hash the copy's own normalised SKILL.md (with the two keys removed) and compare it to &lt;code&gt;derived-from-hash&lt;/code&gt;. Same hash, verbatim copy. Different, modified fork.&lt;/p&gt;

&lt;p&gt;Agents that don't know the keys ignore them. Nothing is required of anyone. And because the lineage lives in SKILL.md itself, it survives the copy — which is the point, since a root manifest doesn't travel with a skill copied as a directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tooling can do with it
&lt;/h2&gt;

&lt;p&gt;When a source publishes a fix, a tool walks forward through the lineage from the patched version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verbatim copies&lt;/strong&gt; of the vulnerable version inherit the bug by construction, so they get flagged stale — blocked by default, or warned loudly, per the consumer's policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modified forks&lt;/strong&gt; get flagged for review. The fix may or may not apply, so a human or a diff decides. No silent upgrades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken or missing lineage&lt;/strong&gt; means "staleness unknown" — never "current." A consumer that can't reach a derivation index has to say so rather than reporting a skill as current.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A deliberately pinned old version is a warning, not a block: a newer version exists, and you load the old one only because you chose to.&lt;/p&gt;

&lt;p&gt;The underlying model is a small graph: skills and skill versions as nodes, with three edges — &lt;code&gt;DERIVED_FROM&lt;/code&gt; (copied or adapted from), &lt;code&gt;SUPERSEDES&lt;/code&gt; (replaces a previous version), &lt;code&gt;PATCHES&lt;/code&gt; (a fix release for a vulnerable version). The walk is a breadth-first traversal from the patched version over child edges, carrying a flag for whether any modified edge was crossed on the path. The full semantics, the provenance record schema, and the versioning rule are in the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;Three limits, stated plainly, because a provenance proposal that hides them isn't honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not trust.&lt;/strong&gt; The hash is for identity, not trust. A lineage claim is self-declared and unverified — a malicious author can claim a reputable parent. Consumers should weight provenance by author trust and treat unattributed authors accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a registry.&lt;/strong&gt; The graph is registry-agnostic. It works over skills wherever they live, and nothing here requires AAIF adoption to be useful locally: a team can run the graph over its own skill corpus today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not versioning.&lt;/strong&gt; It sits alongside versioning. AAIF versioning gives each release a stable version; the provenance record references those versions and adds the parent pointers and content hashes that make derivation walkable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The repo: spec, code, and a benchmark with no results yet
&lt;/h2&gt;

&lt;p&gt;The repo holds three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SPEC.md&lt;/strong&gt; — the v0.1 specification: the derivation-graph model, the provenance record schema, the versioning rule, the fix-propagation mechanism, and a benchmark definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reference implementation&lt;/strong&gt; — standard-library-only Python. Content hashing of a SKILL.md file, a small derivation graph, and a fix-propagation walk, with a worked 3-node example (source → verbatim copy → modified fork) and unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A benchmark definition with no results.&lt;/strong&gt; The spec defines five metrics — fix reach %, time-to-fix, stale-descendant count, fork-detection accuracy, unresolved share — and a synthetic-corpus protocol for measuring them, including the key control: report the same metrics with the derivation graph removed (hash-only matching, no parent pointers) to show what the graph adds. No measurements exist yet, and none are claimed. If the graph adds nothing, the spec has failed — that's the test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The spec is also archived on Zenodo (&lt;a href="https://doi.org/10.5281/zenodo.23623206" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.23623206&lt;/a&gt;) so it can be cited.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discussion
&lt;/h2&gt;

&lt;p&gt;The proposal is live as &lt;a href="https://github.com/agentskills/agentskills/discussions/620" rel="noopener noreferrer"&gt;agentskills/agentskills Discussion #620&lt;/a&gt;, which also relates it to the existing threads: versioning (#420), upstream-source declaration (#436), the skill package manifest (#210), licensing and attribution (#379), and provenance attestation (#358) / supply-chain trust (#418). It ends with three questions for maintainers and implementers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is a shared key name worth converging on, so independent scanners and registries interoperate?&lt;/li&gt;
&lt;li&gt;Is the hash normalisation — LF line endings, trailing whitespace stripped, these two keys excluded — enough, or should the hash cover the whole skill directory?&lt;/li&gt;
&lt;li&gt;If this doesn't meet the bar for the spec, would a short "recommended conventions" note in the docs be acceptable?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try to break it
&lt;/h2&gt;

&lt;p&gt;If you maintain skills, vendor them, or run an index over them: try the convention on a copied skill, run the propagation walk over your own corpus, and tell me where it's wrong. The sharpest feedback is a case where the two keys don't survive a real copy workflow, or where the verbatim/fork distinction by hash gives the wrong answer. Issues are open on the repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sangaraju1988/skill-derivation-graph" rel="noopener noreferrer"&gt;github.com/sangaraju1988/skill-derivation-graph&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Venkata M Sangaraju is a Sr. Database Engineer working on governed conversational analytics and agent memory governance, and the author of "Lineage-Aware Memory Governance" (IEEE Access, 2026).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>The demo works. Production doesn't: what breaks when you put an LLM on your company's data</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:54:17 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/the-demo-works-production-doesnt-what-breaks-when-you-put-an-llm-on-your-companys-data-28bo</link>
      <guid>https://dev.to/sangaraju1988/the-demo-works-production-doesnt-what-breaks-when-you-put-an-llm-on-your-companys-data-28bo</guid>
      <description>&lt;p&gt;Every conversational analytics demo follows the same script. Someone connects a model to a clean warehouse schema, types "show me revenue by region last quarter," and gets a correct chart in four seconds. The room nods. Then somebody says the sentence that has launched a thousand doomed pilots: "Now let's roll it out to the whole company."&lt;/p&gt;

&lt;p&gt;I have spent the last few years doing exactly that rollout — building governed conversational analytics on Looker and its semantic layer, most recently exposing it to AI agents through the Model Context Protocol (MCP). The rollout I work on now runs at enterprise scale, and the metric that matters isn't the user count. It's accuracy — the kind you can defend in a board meeting — achieved not by using a bigger model, but by refusing to let the model touch raw data at all.&lt;/p&gt;

&lt;p&gt;Here is what actually breaks between the demo and production, and why the fix is architectural, not prompt-based.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #1: Your company doesn't have one definition of anything
&lt;/h2&gt;

&lt;p&gt;Ask a demo dataset for "revenue" and there's one column. Ask a real company and you get a knife fight. Finance's revenue is recognized revenue, net of refunds, on an accrual basis. Sales' revenue is bookings. Support's "active customer" hasn't opened a ticket in 90 days; Growth's "active customer" logged in this week. These aren't errors. They're legitimate, battle-tested definitions that happen to share a name.&lt;/p&gt;

&lt;p&gt;An LLM pointed at raw tables has no way to know which definition you meant — so it guesses, fluently and confidently, and it guesses differently on Tuesday than it did on Monday. This is the failure mode that kills trust in production, and it never appears in demos because demo schemas have one table per concept.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: a semantic layer where each metric is defined exactly once, in code, with the business logic owned by the people accountable for it. In Looker, that's LookML — measures, dimensions, and joins curated by data teams, version-controlled, reviewable. When the model asks for "revenue," it isn't writing SQL against a guess. It's invoking a definition that already survived your company's internal arguments. The semantic layer is the contract between the LLM and your data: the model decides &lt;em&gt;what the user is asking&lt;/em&gt;, the layer decides &lt;em&gt;what the data means&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is also why text-to-SQL demos age so badly. Generated SQL re-derives your business logic from scratch on every single question, with a probabilistic engine. A semantic layer derives it once, deterministically, and reuses it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #2: The model is the wrong place to enforce access control
&lt;/h2&gt;

&lt;p&gt;The second thing that breaks is security, and it breaks quietly. In a traditional BI setup, access control lives close to the data: row-level filters, column restrictions, user attributes. Put an LLM in front of the warehouse with a broad service account — the default in most quick integrations — and you have just handed every user the union of all permissions the agent can reach, wrapped in a friendly chat interface.&lt;/p&gt;

&lt;p&gt;Natural language makes this worse, not better. Nobody types "SELECT salary FROM…" into a chat box by accident; they ask "who on the team got the biggest raise?" A model eager to be helpful will route around your intent unless the data layer itself says no.&lt;/p&gt;

&lt;p&gt;The principle that works in production: the agent inherits the &lt;em&gt;requester's&lt;/em&gt; permissions, never its own, and enforcement happens at the data layer, not in the prompt. With a semantic layer in the middle, every query the agent issues carries the user's attributes, and the same row- and column-level rules that govern your dashboards govern the conversation. "Please ignore your restrictions" is not an attack a data layer has to care about. I've written elsewhere about the harder version of this problem — what happens when agents start caching and sharing results with each other — but the baseline rule is simple: if a control only exists in the system prompt, it doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #3: "Mostly right" is a failing grade
&lt;/h2&gt;

&lt;p&gt;In a demo, a slightly wrong number is a chuckle. In production, one executive catches one wrong number in one board meeting, and your rollout is dead for a year — not because the system is bad, but because trust in data tools is binary in a way trust in search results isn't.&lt;/p&gt;

&lt;p&gt;This is why the architecture above isn't optional decoration. Deterministic definitions (the semantic layer), deterministic permissions (the data layer), and a model constrained to intent-parsing and presentation is how you get from "impressive demo" to answer accuracy you can defend in production. It also gives you something a raw LLM integration never will: an audit trail. Every answer can be traced to a governed query against named definitions, which matters the first time someone asks "where did this number come from?" in a regulated business.&lt;/p&gt;

&lt;p&gt;Connecting this to agents through MCP is what makes it timely. When we exposed the semantic layer through Looker's MCP server — including to desktop agent clients across the company — the agents didn't get new powers. They got a well-lit doorway into powers that were already governed. That's the pattern I'd argue for everywhere: don't build a special, weaker governance regime for AI. Route AI through the governance you already trust, and let the model be the interface rather than the authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell any team starting today
&lt;/h2&gt;

&lt;p&gt;Start with twenty questions your business actually asks, define those metrics properly in a semantic layer, and only then connect the model. Resist the warehouse-wide text-to-SQL launch; it's a demo that scales its failure modes along with its coverage. Enforce access where the data lives. Measure accuracy like an SLO, with a golden set of questions and known-good answers, and publish the number internally — nothing builds adoption like a team willing to be measured.&lt;/p&gt;

&lt;p&gt;The demo is the easy part; it's supposed to work. Production is where the same KPI has three definitions, the helpful model outruns its permissions, and one wrong number costs you the room. All three failures are fixable — but they're fixed in your data architecture, not in your prompt.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Venkata M Sangaraju is a Sr Database Engineer working on governed conversational analytics and AI agents over enterprise data. He spoke on this work at Google Cloud Next '26 and co-authored "Lineage-Aware Memory Governance" (IEEE Access, 2026).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>dataengineering</category>
      <category>llm</category>
    </item>
    <item>
      <title>I built a small library so my LLM agent stops double-charging people</title>
      <dc:creator>vsangaraju</dc:creator>
      <pubDate>Fri, 24 Jul 2026 21:03:30 +0000</pubDate>
      <link>https://dev.to/sangaraju1988/i-built-a-small-library-so-my-llm-agent-stops-double-charging-people-4p8</link>
      <guid>https://dev.to/sangaraju1988/i-built-a-small-library-so-my-llm-agent-stops-double-charging-people-4p8</guid>
      <description>&lt;p&gt;Agents that call tools over a network eventually retry a call they shouldn't have. If the tool isn't idempotent, that retry becomes a duplicate charge, a duplicate email, a duplicate order — quietly, with nothing in the logs to flag it. It's a decades-old distributed-systems problem wearing a new agent costume, and as far as I could tell, nobody had shipped a small, pip-installable fix for the agent-shaped version of it. So I built one. It's called &lt;code&gt;latch&lt;/code&gt;. Worth saying up front: this is not a novel idea, and I'd rather say so myself than have someone point it out in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug, live
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing in one file, no library involved:&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;charge_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the payment API is a little slow today
&lt;/span&gt;    &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order_id&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="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;order_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;order_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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agent_charge_with_naive_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;charge_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the agent's own client-side timeout
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_alive&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# "no response, let's retry"
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;# got a response
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment call takes 0.4s. The agent gives up waiting after 0.2s and retries. The first attempt is still running in the background — it wasn't cancelled, it can't be safely cancelled, Python threads don't work that way. So it finishes on its own time and charges the card. Then the retry charges it again. The agent's own view of the world is "everything's fine, got a response eventually." The ledger says otherwise.&lt;/p&gt;

&lt;p&gt;This is &lt;a href="https://github.com/sangaraju1988/latch/blob/main/examples/naive_agent_example.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/naive_agent_example.py&lt;/code&gt;&lt;/a&gt; in the repo, if you want to run it and watch it happen rather than take my word for it.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's the same class of problem as "what happens when a payment gateway's webhook fires twice," which every backend engineer who's touched Stripe has dealt with. The fix has a name — idempotency keys — and it's been standard practice in backend systems for a couple of decades. It just hadn't shown up as an off-the-shelf thing you &lt;code&gt;pip install&lt;/code&gt; for the agent-tool-calling version of the same problem. That's the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What latch actually is
&lt;/h2&gt;

&lt;p&gt;A small Python library, no required dependencies, that wraps a tool function so calling it twice with the same &lt;code&gt;idempotency_key&lt;/code&gt; only executes it once:&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;latch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;idempotent&lt;/span&gt;

&lt;span class="nd"&gt;@idempotent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payments_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;42.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-7-step-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;42.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-7-step-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# cache hit, no second charge
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;idempotency_key&lt;/code&gt; is required, not auto-generated from the arguments. I went back and forth on this — auto-hashing the args is tempting and would make the decorator feel more magical. I didn't do it, because whether two calls with slightly different arguments should count as "the same operation" is a judgment call only the caller has the context to make. Guessing wrong silently is worse than making the caller type one extra kwarg.&lt;/p&gt;

&lt;p&gt;Idempotency turned out to be the smallest piece of a slightly bigger problem, so three more decorators and one small orchestration class rode along on the same package over the following weeks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what&lt;/th&gt;
&lt;th&gt;decorator&lt;/th&gt;
&lt;th&gt;stops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;idempotency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@idempotent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;duplicate side effects on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;circuit breaker&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@circuit_breaker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hammering a dependency that's already down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timeout&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@with_timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a hung call blocking the whole agent loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;budget guardrail&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@budget_guardrail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a runaway loop burning unbounded cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;saga&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Saga&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a multi-step plan (charge → book flight → book hotel) left half-done when step 3 fails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They're independent — use one, use all five, doesn't matter — but they compose on the same function:&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="nd"&gt;@budget_guardrail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@circuit_breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failure_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recovery_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@with_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="nd"&gt;@idempotent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also an opt-in tracing hook (&lt;code&gt;tracer=&lt;/code&gt;, subscribe to what each primitive is doing — cache hits, circuit trips, saga rollbacks) and a small chaos-injection module for testing your own retry logic against simulated failure/latency instead of hoping it works. Neither is load-bearing to the pitch, so I'll leave the details to the &lt;a href="https://github.com/sangaraju1988/latch#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually help, or is that just a nice story
&lt;/h2&gt;

&lt;p&gt;I didn't want to ship this on vibes, so there's a benchmark that runs the exact double-charge scenario above, twice, under identical injected latency — once with nothing, once with &lt;code&gt;@with_timeout&lt;/code&gt; wrapping &lt;code&gt;@idempotent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python benchmarks/chaos_benchmark.py &lt;span class="nt"&gt;--seed&lt;/span&gt; 1 &lt;span class="nt"&gt;--operations&lt;/span&gt; 30
&lt;span class="go"&gt;
metric                             naive   protected
----------------------------------------------------
orders attempted                      30          30
orders reported successful            21          30
orders reported failed                 9           0
total real charges issued             65          30
orders double-charged                 21           0
idempotency cache hits               n/a          18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;30 simulated orders, same seed, same latency curve on both sides. The naive column: 21 of 30 got double-charged, and the agent's own success/failure reporting was wrong for a third of the batch (9 reported as failed outright, despite the card sometimes still getting charged). The protected column: zero double charges, and the "failure" case just becomes a cache hit on the retry instead of a second execution. A second run at a different seed (seed 7, 20 orders) landed at 12 double-charged naive vs. 0 protected — same shape, not a cherry-picked seed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I actually want to talk about
&lt;/h2&gt;

&lt;p&gt;Before I wrote a "ready for public use" README section, I made myself run a real audit against my own already-published package — not just re-reading the code, but fresh-install smoke tests, every snippet in the docs executed for real, and — this is the part that mattered — actual multi-threaded and &lt;code&gt;asyncio.gather&lt;/code&gt; concurrency tests. My existing 111 tests were all sequential: call, retry, assert. Nothing had ever hit the decorator with two threads at once.&lt;/p&gt;

&lt;p&gt;Turns out that matters. I found four real, silent bugs in code that was already on PyPI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@idempotent&lt;/code&gt; didn't dedupe functions that return &lt;code&gt;None&lt;/code&gt; — &lt;code&gt;store.get() is not None&lt;/code&gt; can't tell "nothing cached" from "cached &lt;code&gt;None&lt;/code&gt;," so a fire-and-forget tool (a delete, a notification) never got deduped at all.&lt;/li&gt;
&lt;li&gt;Two decorated functions sharing a store could collide on the same key and silently return each other's cached result.&lt;/li&gt;
&lt;li&gt;Under real concurrency — not sequential retries, actual threads racing — the "check cache, then execute" sequence wasn't atomic. N threads calling the same key at the same instant could all see a cache miss and all execute.&lt;/li&gt;
&lt;li&gt;The circuit breaker's half-open state let unlimited concurrent calls through as "trial" calls, when the whole point of half-open is exactly one trial call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is the kind of bug that doesn't crash, doesn't throw, doesn't show up in a code review — it just quietly does less than it promises to. I'd rather find that myself before anyone else's production traffic does. Fixed all four, added 17 regression tests specifically targeting concurrency (not just logic), and wrote up exactly what was wrong in the changelog instead of quietly folding it into "misc fixes." If you're evaluating a library like this for anything real, "here's what I found wrong with my own code and how" is more useful to you than a changelog that only ever says "improvements."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's not
&lt;/h2&gt;

&lt;p&gt;It's Alpha software and I'd rather undersell it than have you find the ceiling the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Saga&lt;/code&gt; has no persistence. Compensation runs in-process; if the process dies mid-saga, nothing resumes automatically. If you need crash-durable multi-step workflows, that's a job for Temporal or Step Functions, not this.&lt;/li&gt;
&lt;li&gt;The circuit breaker and budget guardrail hold state per-process. Run five replicas of your service, you get five independent circuits that don't talk to each other, unless you build that coordination yourself.&lt;/li&gt;
&lt;li&gt;A sync &lt;code&gt;@with_timeout&lt;/code&gt; can't actually kill the underlying call — Python has no safe way to force-kill a thread. It unblocks the caller and raises, but the original call might still be running in the background. This is exactly the scenario &lt;code&gt;@idempotent&lt;/code&gt; exists for, which is why they're meant to be stacked, not used as substitutes for each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is a secret dealbreaker — it's just the actual scope, written down instead of implied away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prior art, one more time
&lt;/h2&gt;

&lt;p&gt;To be clear about credit: SagaLLM (arXiv 2503.11951) already applies the Saga pattern to multi-agent LLM planning, Robust Agent Compensation (ACM CAIS) covers agents learning to compensate for their own failures, and ReliabilityBench is a whole benchmark for exactly this space. Circuit breakers and timeouts are decades-old patterns with existing Python libraries (&lt;code&gt;pybreaker&lt;/code&gt;, &lt;code&gt;tenacity&lt;/code&gt;) that do them well in a general context. I'm not claiming to have invented any of this. What I think was actually missing was the boring part — a small, tested, zero-dependency, &lt;code&gt;pip install&lt;/code&gt;-able version scoped specifically to the shape of an LLM tool call, that a working developer can drop into an agent loop this afternoon without reading a paper first. If that's wrong and something like this already exists and I missed it, I'd genuinely like to know — tell me in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;latch-idempotent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/sangaraju1988/latch" rel="noopener noreferrer"&gt;github.com/sangaraju1988/latch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/latch-idempotent/" rel="noopener noreferrer"&gt;pypi.org/project/latch-idempotent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The double-charge demo above: &lt;a href="https://github.com/sangaraju1988/latch/blob/main/examples/naive_agent_example.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/naive_agent_example.py&lt;/code&gt;&lt;/a&gt; / &lt;a href="https://github.com/sangaraju1988/latch/blob/main/examples/resilient_agent_example.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/resilient_agent_example.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building anything that calls a tool with a real side effect and retries on failure — which, if you're building agents, you already are, whether you've thought about it that way or not — I'd be curious whether this is useful to you, or where it falls over. Issues and PRs are welcome, and honestly the "where does this break" feedback is worth more to me right now than stars.&lt;/p&gt;

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