<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Sam Reghenzi Homepage</title><link href="https://blog.r6i.it/" rel="alternate"/><link href="https://blog.r6i.it/feeds/all.atom.xml" rel="self"/><id>https://blog.r6i.it/</id><updated>2026-09-01T09:56:00+02:00</updated><entry><title>The Ontology Was Always a Bounded Context</title><link href="https://blog.r6i.it/ontology-was-always-a-bounded-context.html" rel="alternate"/><published>2026-09-01T09:56:00+02:00</published><updated>2026-09-01T09:56:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-09-01:/ontology-was-always-a-bounded-context.html</id><summary type="html">&lt;p&gt;Two agents meet in a park.&lt;/p&gt;
&lt;p&gt;"I've just had a complaint from a customer," says the first.&lt;/p&gt;
&lt;p&gt;"What's a customer?" asks the second.&lt;/p&gt;
&lt;p&gt;"Anyone with an account."&lt;/p&gt;
&lt;p&gt;A third agent walks over.&lt;/p&gt;
&lt;p&gt;"That's not a customer," it says. "A customer is anyone with an active subscription."&lt;/p&gt;
&lt;p&gt;The three agents pause …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Two agents meet in a park.&lt;/p&gt;
&lt;p&gt;"I've just had a complaint from a customer," says the first.&lt;/p&gt;
&lt;p&gt;"What's a customer?" asks the second.&lt;/p&gt;
&lt;p&gt;"Anyone with an account."&lt;/p&gt;
&lt;p&gt;A third agent walks over.&lt;/p&gt;
&lt;p&gt;"That's not a customer," it says. "A customer is anyone with an active subscription."&lt;/p&gt;
&lt;p&gt;The three agents pause.&lt;/p&gt;
&lt;p&gt;Nobody is wrong. Nobody is hallucinating.&lt;/p&gt;
&lt;p&gt;They are just using the same word to mean different things.&lt;/p&gt;
&lt;p&gt;And somewhere between them, an integration is about to break.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Three agents in a park, each defining &amp;quot;customer&amp;quot; differently" src="/images/xkcd_style_agents_customer_en.png"&gt;&lt;/p&gt;
&lt;p&gt;This is the failure mode that is currently being rebranded as "agents need ontologies." The diagnosis is right. The fix being marketed alongside it is not. That fix is a single shared ontology sitting above the agents, arbitrating meaning for everyone — and it's the wrong prescription for a problem that already has a name and a twenty-year-old solution. The name is Domain-Driven Design. It isn't showing up in this conversation because most of the people having it came from data and knowledge graphs, not from software architecture.&lt;/p&gt;
&lt;h2&gt;The problem is real and it's measurable&lt;/h2&gt;
&lt;p&gt;Start with the evidence, because the failure isn't hypothetical. OG-RAG, presented at EMNLP 2025, anchors retrieval to an ontology-backed hypergraph instead of a flat vector index. It reports a 55% jump in fact recall and 40% in answer correctness over standard RAG on the same corpus. A 2026 biomedical study went further. Grounding agent outputs against a domain ontology in RDF/OWL brought hallucination rates on clinical queries down from 63% and 48%, depending on the baseline, to 1.7% — with accuracy around 98%.&lt;/p&gt;
&lt;p&gt;Numbers like that explain why every vendor with a graph database is suddenly an "ontology for agentic AI" company. They also explain why the instinct isn't wrong. Give an LLM-based system an explicit, checkable model of the domain it operates in, and it gets measurably more reliable. I made a version of this argument in &lt;a href="/ontologies-agentic-guardrails.html"&gt;The Guardrail Is Not in the Model&lt;/a&gt;: the check that catches an illegal state has to live outside the model, because a probability distribution cannot reliably remember a rule that was never written down.&lt;/p&gt;
&lt;p&gt;What that post didn't answer is the next question, which is organizational rather than architectural. Whose ontology? Covering what? Owned by whom, and updated how? That's where the current wave of enterprise ontology platforms gives an answer I don't think holds up.&lt;/p&gt;
&lt;h2&gt;The diagnosis that keeps being wrong&lt;/h2&gt;
&lt;p&gt;The answer on offer — from Palantir's Foundry Ontology to Stardog to RelationalAI — is some version of: build one semantic model of the enterprise, wire every agent to it, and let it be the shared source of truth.&lt;/p&gt;
&lt;p&gt;I have seen this movie. It's the Semantic Web, 2001 to roughly 2010, W3C-flavored instead of venture-flavored. RDF, OWL, a vision of universally linked data with formally specified meaning. Then a slow, expensive collapse. The reason it failed was never that formal semantics don't work; SHACL validators are boring, cheap, and effective at exactly the scale you point them at. It failed because getting an organization to agree on one model of everything is brutally expensive, and keeping that model true over time is worse.&lt;/p&gt;
&lt;p&gt;The new version does have one genuinely different cost: authoring. An LLM can draft a plausible ontology from your documentation and schemas for a few cents, where a knowledge engineer used to bill for weeks. That's real progress. But authoring was never the part that killed the Semantic Web. Governance was — what happens once two teams' understanding of "customer" starts to drift apart in production. Cheaper to build is not the same as cheaper to own, and owning is where a global ontology dies.&lt;/p&gt;
&lt;h2&gt;DDD already named this problem&lt;/h2&gt;
&lt;p&gt;Here is what the ontology-for-agents conversation is missing: this is not a new problem. Eric Evans described it in &lt;em&gt;Domain-Driven Design&lt;/em&gt; in 2003 — different parts of an organization using the same word to mean different things. His answer was never "build one model and force everyone onto it."&lt;/p&gt;
&lt;p&gt;His answer was the &lt;strong&gt;bounded context&lt;/strong&gt;: a model only has to be internally consistent within an explicit boundary. Inside the billing context, "customer" means "entity with an active subscription," full stop, and every rule is coherent under that definition. Inside the support context, "customer" means "entity with an account," and that's equally coherent on its own terms. The two contexts are not wrong relative to each other. They were never supposed to share a definition in the first place.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ubiquitous language&lt;/strong&gt; maps directly onto what an ontology actually is when it's working. It's not a separate artifact that sits above the code and gets synced with it by a process nobody enjoys. It &lt;em&gt;is&lt;/em&gt; the vocabulary the domain experts and the model use, inside that one context, for that one purpose. An ontology that lives outside the bounded context it describes is exactly the kind of artifact DDD spent a book telling you not to build.&lt;/p&gt;
&lt;p&gt;And when two contexts genuinely need to talk, DDD already has the vocabulary for that too. A &lt;strong&gt;shared kernel&lt;/strong&gt; for the small slice of model both sides agree to co-own. An &lt;strong&gt;anticorruption layer&lt;/strong&gt; when one side must consume the other's model without importing its assumptions. A &lt;strong&gt;partnership&lt;/strong&gt; when two teams agree to evolve their contexts in step. Read the current papers on "multi-agent semantic interoperability" and you are reading context mapping with the serial numbers filed off.&lt;/p&gt;
&lt;h2&gt;Event-driven architecture already named the other half&lt;/h2&gt;
&lt;p&gt;Bounded contexts keep a local model coherent. They don't explain how contexts stay in sync without collapsing back into one shared schema. That half was solved by event-driven architecture.&lt;/p&gt;
&lt;p&gt;The core discipline is treating an event as a fact, not a command: &lt;code&gt;OrderPlaced&lt;/code&gt;, not &lt;code&gt;PlaceOrder&lt;/code&gt;. A command presupposes that the receiver shares your model closely enough to know what to do with the instruction. A fact just asserts that something happened, in the vocabulary of the context that emitted it, and lets every consumer interpret it inside its own model. That is far looser coupling than a synchronous call between two agents that must agree, in real time, on one shared schema just to complete a request.&lt;/p&gt;
&lt;p&gt;Event sourcing covers the other thing the enterprise-ontology pitch leads with: audit and accountability. A well-designed event log is already a complete, ordered, immutable record of every fact the system has asserted. That is precisely the traceability that Palantir and Stardog sell as the differentiated value of an ontology layer. You don't need a global semantic model to get an audit trail. You need an event log that nobody is allowed to mutate.&lt;/p&gt;
&lt;p&gt;Which makes it a little dispiriting to watch the current agent stack rebuild what event-driven architecture spent twenty years moving away from. MCP, launched by Anthropic in late 2024, is a good protocol for what it does: a stable way for a model to discover and call a tool. But the pattern forming on top of it — and on top of LangGraph, CrewAI, and AutoGen — is a central orchestrator calling each agent synchronously, with a shared understanding of the payload assumed on both ends. Distributed systems spent two decades learning to prefer choreography over that. The agent stack is reinventing the thing we walked away from.&lt;/p&gt;
&lt;h2&gt;Explicit structure isn't a compromise, it's a multiplier&lt;/h2&gt;
&lt;p&gt;There's an objection worth taking seriously here: agents aren't microservices. A microservice's behavior is deterministic given its inputs; an LLM's isn't. Doesn't imposing a rigid schema on an inherently stochastic process miss the point?&lt;/p&gt;
&lt;p&gt;It's the opposite. Precisely &lt;em&gt;because&lt;/em&gt; the output is stochastic, giving it an explicit shape to land in is what makes it usable. This isn't hand-waving — it's what the numbers at the top of this post already showed. Constraining generation against an explicit ontology is what took the biomedical hallucination rate from the 50-60% range to 1.7%. Structure doesn't fight the model's nature; it compensates for it. A type system doesn't fight a language's flexibility either. It gives the parts that need to be reliable somewhere firm to stand.&lt;/p&gt;
&lt;p&gt;There's a second benefit that's easy to undersell: introspection. An explicit, local ontology gives you a place to &lt;em&gt;look&lt;/em&gt; when an agent's behavior needs explaining, in a way a free-text prompt never does. When an agent inside the billing context takes an action, you can ask which entities and relations it reasoned over, and check the answer against a schema — instead of re-reading a transcript and guessing. That extends the argument from &lt;a href="/ontologies-agentic-guardrails.html"&gt;The Guardrail Is Not in the Model&lt;/a&gt; one step further. The structure that lives outside the model isn't only a cage that stops bad states. It's an instrument that makes the good ones legible.&lt;/p&gt;
&lt;h2&gt;Who maintains it: the review loop, not the curator&lt;/h2&gt;
&lt;p&gt;The objection that actually matters is the one that killed the Semantic Web: someone has to own this thing over time, and ownership is expensive. DDD's answer was never "write the model once." A mature bounded context goes through &lt;em&gt;model refinement&lt;/em&gt; as the team's understanding deepens. The ubiquitous language evolves because the domain does, not because someone forgot to finish it the first time.&lt;/p&gt;
&lt;p&gt;What's different now is who drafts the refinement. In the expert-systems era, and in the Semantic Web's, a human had to notice the drift, understand it, and hand-author the fix. Today the agent itself can be the one to notice. It can flag the cases where its actions kept getting rejected by the local schema, or where two contexts' events stopped lining up, and propose a specific, scoped patch to the local ontology. A human still reviews and merges the diff. What disappears is the step where a person has to notice the drift unassisted and draft the fix from a blank page.&lt;/p&gt;
&lt;p&gt;Note the constraint doing the work here: the patch is scoped to one bounded context, not to a global model. That's what keeps the loop tractable. This is a real reduction in the maintenance cost that killed the last two attempts at formal semantics, and it only exists because the thing being maintained is small.&lt;/p&gt;
&lt;h2&gt;The provocation&lt;/h2&gt;
&lt;p&gt;None of this requires a new discipline called "ontology engineering for agentic AI." It requires applying to agents the discipline distributed systems already have. Give each agent a bounded context and an explicit local model, not a slice of a shared global one. Connect contexts with events that are facts, not commands that presuppose a shared schema. Run a periodic, LLM-assisted refinement loop instead of pretending the model will be right forever once you've paid to build it.&lt;/p&gt;
&lt;p&gt;So before buying a platform that sells itself as "the enterprise ontology layer for your agents," ask a blunter question: is the missing piece really a new epistemic layer, or is it a bounded context design, an event bus, and a review process you haven't built yet? In most of the failures I've seen, it's the second one. And it was always going to be cheaper.&lt;/p&gt;</content><category term="AI"/><category term="AI"/><category term="LLM"/><category term="ontology"/><category term="DDD"/><category term="agents"/><category term="MCP"/></entry><entry><title>The Guardrail Is Not in the Model</title><link href="https://blog.r6i.it/ontologies-agentic-guardrails.html" rel="alternate"/><published>2026-08-30T14:16:00+02:00</published><updated>2026-08-30T14:16:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-08-30:/ontologies-agentic-guardrails.html</id><summary type="html">&lt;p&gt;Frank Coyle gave &lt;a href="https://www.youtube.com/watch?v=Sir59K8ZDPU"&gt;a twenty-minute talk&lt;/a&gt; at the AI Engineer World's Fair that has been circulating for a few weeks now, and the argument in it is simple enough to state in one sentence: agentic systems need ontologies, because LLMs are probabilistic and probabilistic systems need logical guardrails.&lt;/p&gt;
&lt;p&gt;I think …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Frank Coyle gave &lt;a href="https://www.youtube.com/watch?v=Sir59K8ZDPU"&gt;a twenty-minute talk&lt;/a&gt; at the AI Engineer World's Fair that has been circulating for a few weeks now, and the argument in it is simple enough to state in one sentence: agentic systems need ontologies, because LLMs are probabilistic and probabilistic systems need logical guardrails.&lt;/p&gt;
&lt;p&gt;I think he is right, but the interesting part is why, and that is easier to see if you start from the failures rather than from the ontologies.&lt;/p&gt;
&lt;h2&gt;The failure that isn't a hallucination&lt;/h2&gt;
&lt;p&gt;Take a support agent that handles refunds. A request comes in, the agent reads the order, calls the payments tool, and refunds €340 to the customer. The tool call is well-formed, the arguments validate, and the summary it writes afterwards is an accurate description of what it did. The problem is that the same order had already been refunded four days earlier.&lt;/p&gt;
&lt;p&gt;It is worth going through the layers that should have caught this, because none of them did. The model did not hallucinate, since it never stated anything false. Schema validation passed: the amount was a positive decimal, the order ID was a UUID, and the reason was one of the allowed values. The eval suite passed too, because it scores the agent's output for helpfulness and faithfulness, and as a piece of text the response was fine. The system prompt did say not to issue duplicate refunds, and the model would have confirmed that rule if you had asked it.&lt;/p&gt;
&lt;p&gt;The failure was not in the language. It was that "an order has at most one refund" is a rule of the business that existed only in the heads of a few engineers and in the implicit assumptions of some older database queries. It was never written down anywhere a machine could check it.&lt;/p&gt;
&lt;p&gt;Most agent incidents I have seen have this shape. They are not fabrications, they are illegal states that nobody had written down. A payout goes to a support rep instead of the buyer because the recipient field was typed as a string. An order ends up with the status &lt;code&gt;probably_shipped&lt;/code&gt; because the tool accepted free text. A category becomes its own ancestor because nothing said the relation was acyclic.&lt;/p&gt;
&lt;p&gt;The usual responses to this all have something in common. Longer system prompts, more context, better retrieval, another round of fine-tuning: every one of them is a change inside the model, and every one asks a probability distribution to reliably avoid a state that was never defined. It is an attempt to solve a logical problem with more probability.&lt;/p&gt;
&lt;h2&gt;The loop is where the power and the risk both come from&lt;/h2&gt;
&lt;p&gt;There is a point in Coyle's talk that puts this in a useful frame.&lt;/p&gt;
&lt;p&gt;In 1966 Böhm and Jacopini proved that any language with three constructs — sequence, conditional, and iteration — can compute anything computable. The then-current argument about whether Fortran or COBOL was the better language was therefore beside the point, since both had all three.&lt;/p&gt;
&lt;p&gt;Until recently an LLM call gave you sequence and, with tool use, something like a conditional. Then we added the loop, in the form of &lt;code&gt;while stop_reason == "tool_use"&lt;/code&gt;. That third construct is a large part of why agents can now do things that a 2023 chat interface could not.&lt;/p&gt;
&lt;p&gt;Along with the power we inherited the familiar problems of loops. They can fail to terminate. They can drift, with small errors compounding so that each turn is slightly further from the goal than the last and no single step looks wrong. They can consume an unbounded amount of money, which is why token budgets exist.&lt;/p&gt;
&lt;p&gt;So we have a Turing-complete system whose transition function is a neural network, operating over a state space that was never formally specified, and our main way of constraining it is a system prompt written in English.&lt;/p&gt;
&lt;p&gt;That is not a criticism of LLMs. It is the same reason we do not secure a web application by asking people in the documentation not to attempt SQL injection. A constraint that the system has to pass through is different in kind from a constraint the system is asked to remember.&lt;/p&gt;
&lt;h2&gt;What an ontology is, in practical terms&lt;/h2&gt;
&lt;p&gt;The word carries a lot of philosophical history, from Aristotle's categories through Quine to the Semantic Web work of the 2000s. Coyle sensibly reaches for Gruber's 1993 definition, &lt;em&gt;a formal specification of a shared conceptualization&lt;/em&gt;. For engineering purposes it can be made more concrete than that.&lt;/p&gt;
&lt;p&gt;The first part is a &lt;strong&gt;graph&lt;/strong&gt;: entities, the relations between them, and properties on both. Order, Customer, Refund, SupportRep, &lt;code&gt;refundOf&lt;/code&gt;, &lt;code&gt;paidTo&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;. If you have ever drawn your domain on a whiteboard, you have already done most of this work.&lt;/p&gt;
&lt;p&gt;The second part is a &lt;strong&gt;set of constraints that sits beside the graph&lt;/strong&gt; rather than inside it. This is the part that usually gets skipped, and it is where most of the value is. Graph technology lets you state things about the shape of the graph and then check them mechanically: that &lt;code&gt;status&lt;/code&gt; takes one of exactly three values, that an Order has at most one Refund, that the recipient of a payout must be a Customer and that nothing is ever both a Customer and a SupportRep, that &lt;code&gt;parentCategoryOf&lt;/code&gt; is transitive so a category cannot end up inside itself.&lt;/p&gt;
&lt;p&gt;Some of these statements let you &lt;strong&gt;derive&lt;/strong&gt; facts that were never written down. If the source of &lt;code&gt;teaches&lt;/code&gt; is always a Teacher, then the single assertion "Bob teaches Scooter" tells you that Bob is a teacher and Scooter a student without either being stated. Other statements let you &lt;strong&gt;reject&lt;/strong&gt; a state. Both are useful, and they are not the same operation, which matters later.&lt;/p&gt;
&lt;p&gt;The structural point is that these rules live neither in the data nor in the weights. Not in the rows, because a relational schema can express a foreign key but not "at most one refund per order across these three tables." Not in the model, because fine-tuning adjusts a distribution rather than adding a decision procedure. They live in a third place, and that third place can be checked cheaply by ordinary code.&lt;/p&gt;
&lt;p&gt;That is the architectural argument. The rest is implementation.&lt;/p&gt;
&lt;h2&gt;This has been tried before&lt;/h2&gt;
&lt;p&gt;If this sounds familiar, it should. It was tried in the 1980s and it failed.&lt;/p&gt;
&lt;p&gt;Symbolic AI then meant expert systems: knowledge engineers sitting with domain experts, extracting rules and encoding them by hand. A lot of money went into it, Japan launched the Fifth Generation project, and then the field collapsed into the AI winter. The usual summary is that it did not scale.&lt;/p&gt;
&lt;p&gt;Coyle mentions this and moves on, but the reason it did not scale is worth pausing on, because it determines whether we are about to repeat the mistake.&lt;/p&gt;
&lt;p&gt;Expert systems did not fail because formal logic is useless. They failed at knowledge acquisition. The bottleneck was a person translating a domain expert's tacit knowledge into rules, one rule at a time, and then maintaining those rules as the domain changed underneath them. The reasoning was the cheap part. Getting the knowledge in was the hard part.&lt;/p&gt;
&lt;p&gt;That is precisely the part that has changed. An LLM is good at reading unstructured material — tickets, documentation, transcripts, code, schemas — and proposing structure from it. A validator is good at the thing LLMs are bad at, which is saying no reliably.&lt;/p&gt;
&lt;p&gt;So the division of labour has swapped. In 1985 humans did the acquisition and machines did the inference. Now the model does the acquisition and the machine does the verification, with humans reviewing a diff rather than authoring a corpus. That is a different bet from the one that failed.&lt;/p&gt;
&lt;p&gt;There is a cost that tends to go unmentioned, though: someone has to own the ontology. When the business introduces partial refunds, "at most one refund per order" stops being true, and if nobody updates the rule the guardrail starts blocking legitimate work. A stale ontology is worse than no ontology, because it fails on correct behaviour and the team learns to route around it. This is a schema, and schemas need owners and a migration process.&lt;/p&gt;
&lt;h2&gt;Where it goes in the loop&lt;/h2&gt;
&lt;p&gt;Here is the agentic loop, with the place for the check marked.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_request&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;claude-opus-5&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;adaptive&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tool_use&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;assistant&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tool_use&amp;quot;&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;# 1. Shape. Pydantic at the door: is this even the right type?&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_MODELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Invalid arguments: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&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;# 2. Effect — computed, NOT committed. Pure function, no side effects.&lt;/span&gt;
        &lt;span class="n"&gt;proposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_IMPLS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Legality. The ontology at the ledger: is this state allowed?&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_triples&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;verdict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explain&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;# 4. Only now does anything leave the process.&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proposed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Coyle has a good way of summarising this: Pydantic at the door, the ontology at the ledger. The two answer different questions. Pydantic asks whether the arguments have the right shape, so that a decimal appears where a decimal belongs. The ontology asks whether the resulting state is legal, and that question can only be asked about the proposed effect rather than the arguments. This is why step 2 has to be a pure function. An agent that writes to the database and then validates has already lost the ability to refuse; the check has to happen while the action is still a hypothesis.&lt;/p&gt;
&lt;p&gt;The other thing worth noting is what happens on rejection. The verdict goes back to the model as a tool result together with an explanation, so the agent is told that its move was not allowed and why, and it takes another turn. Nothing crashes. If the same rejection repeats, escalate to a human.&lt;/p&gt;
&lt;p&gt;The validator itself is short:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;rdflib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Graph&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyshacl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;

&lt;span class="n"&gt;DOMAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;shop.ttl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;turtle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# the graph itself&lt;/span&gt;
&lt;span class="n"&gt;SHAPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;shop-shapes.ttl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;turtle&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# the constraints&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposed_triples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Merge the proposed effect into the domain graph and ask whether the&lt;/span&gt;
    &lt;span class="c1"&gt;# result is a legal world. Nothing here touches the database.&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DOMAIN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;proposed_triples&lt;/span&gt;
    &lt;span class="n"&gt;conforms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shacl_graph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SHAPES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;advanced&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&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;Verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conforms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# shop-shapes.ttl&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;OrderShape&lt;/span&gt; &lt;span class="kt"&gt;a&lt;/span&gt; &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;NodeShape&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;targetClass&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;property&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;status&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;paid&amp;quot;&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;shipped&amp;quot;&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;refunded&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;# kills probably_shipped&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;maxCount&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;property&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;inversePath&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;refundOf&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;maxCount&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;                            &lt;span class="c"&gt;# kills the second refund&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;message&lt;/span&gt; &lt;span class="s"&gt;&amp;quot;Order already has a refund.&amp;quot;&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;span class="nt"&gt;PayoutShape&lt;/span&gt; &lt;span class="kt"&gt;a&lt;/span&gt; &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;NodeShape&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;targetClass&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;Payout&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;property&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;paidTo&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c"&gt;# kills the payout sent to a support rep&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;minCount&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nn"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nt"&gt;maxCount&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is where the distinction between deriving and rejecting starts to matter, because most graph tooling was designed for the first of the two.&lt;/p&gt;
&lt;p&gt;Graph technology grew up on the open web, where a graph is partial by construction: you hold a fragment of something larger, and the absence of a fact means unknown rather than false. Reasoners built on that assumption are generous. Tell one that an order can have at most one refund and then assert two, and it will not complain. It will conclude that the two refunds must be the same refund. That is sound reasoning under the assumption it was given, and it is the wrong answer here.&lt;/p&gt;
&lt;p&gt;A refund ledger is the opposite kind of graph. It is closed and complete, and absence does mean false: if a second refund is not in it, there is no second refund. What you want for that is constraint validation rather than inference — cardinality, permitted values, required types — checked against a graph you have declared to be complete. That is what the shapes above do, and it is why the validator is short and contains no reasoner. Validation also produces a structured violation report, which is exactly what you want to hand back to the model as a tool result.&lt;/p&gt;
&lt;p&gt;Inference is still useful for other things: transitive closure over a hierarchy, deriving types you did not assert, surfacing a contradiction you would not have thought to query for. Treat it as a second layer, added deliberately. The check that stops the duplicate refund is the validator, so if you build only one of the two, build that.&lt;/p&gt;
&lt;p&gt;It is also worth not starting from scratch. &lt;code&gt;schema.org&lt;/code&gt; has a substantial vocabulary for commerce, people and events, and reusing it means your ontology carries meaning outside your own codebase.&lt;/p&gt;
&lt;h2&gt;What this does not fix&lt;/h2&gt;
&lt;p&gt;An ontology does not make the agent smarter. It reduces the set of actions the agent can take without being stopped. Those are different properties, and the second one is the one you can verify.&lt;/p&gt;
&lt;p&gt;It catches illegal states, not mistakes of judgment. A refund of €340 that should have been €34 is entirely legal: right type, right recipient, no duplicate, valid status. The ontology has nothing to say about it, and the same is true of a decision to refund a customer who was not entitled to one. Every constraint you can write down is a constraint you already knew, so the guardrail covers exactly as much as your explicit knowledge and no more.&lt;/p&gt;
&lt;p&gt;That gives a reasonable scoping rule. If your domain has no constraints you can write down, this layer is not for you. A summarisation agent, a research assistant or a code explainer has no ledger and no illegal state, so validation only adds latency and maintenance. The technique is worth the effort where the agent moves money, changes records, sends things to people or drives a state machine — that is, where "wrong" has a definition that survives being written down.&lt;/p&gt;
&lt;p&gt;That is a smaller set than all agents, but it is close to the set of agents people are currently trying to put into production.&lt;/p&gt;
&lt;p&gt;The neuro-symbolic framing is fashionable, and I am generally wary of it because it usually arrives as an architecture diagram rather than as a claim you can test. The practical version is more modest: write down the rules of your domain in a form a machine can check, put the check between the agent's intent and its effect, and stop relying on the model to remember them.&lt;/p&gt;</content><category term="AI"/><category term="AI"/><category term="LLM"/><category term="ontology"/><category term="agents"/><category term="python"/><category term="claude"/></entry><entry><title>The Baseline That Refuses to Die</title><link href="https://blog.r6i.it/classical-ml-2026-baselines.html" rel="alternate"/><published>2026-07-31T09:30:00+02:00</published><updated>2026-07-31T09:30:00+02:00</updated><author><name>Sam Reghenzi</name></author><id>tag:blog.r6i.it,2026-07-31:/classical-ml-2026-baselines.html</id><summary type="html">&lt;p&gt;For a decade, gradient-boosted trees owned tabular machine learning. In the last twelve months tabular foundation models finally — and decisively — beat them on TabArena. But the lesson practitioners are drawing ("stop using trees") is almost the opposite of what the evidence supports, because almost nobody runs the comparison correctly. A field guide to baselines, benchmarking discipline, and the GPU crunch in 2026.&lt;/p&gt;</summary><content type="html">&lt;p&gt;For roughly a decade, tabular machine learning had a boring, reliable answer. Someone would hand you a CSV — churn labels, credit defaults, sensor readings, insurance claims — and you would reach for a gradient-boosted tree. XGBoost, LightGBM, or CatBoost. Every eighteen months a deep learning paper would claim to have finally beaten the trees, practitioners would find the comparison unfair, and boosting would keep its throne.&lt;/p&gt;
&lt;p&gt;That era ended sometime in the last twelve months, and it ended more decisively than most people realize. But the conclusion practitioners are drawing from it — &lt;em&gt;stop using trees&lt;/em&gt; — is close to the opposite of what the evidence supports.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What actually changed&lt;/h2&gt;
&lt;p&gt;The venue for this argument is now TabArena, a living, Elo-rated benchmark over 51 curated tabular datasets with proper tuning protocols and repeated splits. It is the first tabular benchmark designed by people who had clearly been burned by bad tabular benchmarks.&lt;/p&gt;
&lt;p&gt;On that board, tabular foundation models — pretrained transformers that do in-context learning over your rows, no gradient steps required — now hold every top single-model slot. In Prior Labs' own technical report, TabPFN-3 in its default configuration lands around 1677 Elo, with a "thinking" variant well above that; AutoGluon's four-hour multi-model ensemble pipeline sits at 1695. The best trees, tuned &lt;em&gt;and&lt;/em&gt; post-hoc ensembled, come in around LightGBM 1440, CatBoost 1414, XGBoost 1387. On the small-data slice, the foundation model gets to a statistical tie with the four-hour AutoGluon pipeline in a single forward pass.&lt;/p&gt;
&lt;p&gt;Then on June 30, 2026, Google Research released TabFM, a zero-shot tabular foundation model that also beats heavily tuned trees on TabArena — reportedly by solving the pretraining-data problem with large-scale synthetic table generation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The comparison nobody runs&lt;/h2&gt;
&lt;p&gt;Here is the uncomfortable part. Almost every team that has "evaluated foundation models against XGBoost" this year did not run a comparison. They ran a demo.&lt;/p&gt;
&lt;p&gt;The typical version looks like this: load the dataset, call a foundation model's &lt;code&gt;predict&lt;/code&gt;, compare against an XGBoost with roughly default hyperparameters and a single train/test split, observe that the foundation model wins by two points of AUC, and write it up. This is exactly the mistake the tabular deep learning literature spent 2021–2024 making in the other direction, and it is worth being precise about the failure modes, because they compound.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tuning budget parity.&lt;/strong&gt; The two families have asymmetric returns on tuning: a foundation model's default is often within noise of its tuned version, while a GBDT's default sits well below tuned, and tuned-plus-ensembled is another step above that. "50 Optuna trials each" hands one side a budget it cannot spend and the other side less than production practice. State the budget in wall-clock or dollars, and report defaults and tuned separately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ensembling parity.&lt;/strong&gt; Post-hoc weighted ensembling of configurations is nearly free accuracy and it reorders tabular leaderboards. If the challenger's number is 32 blended forward passes and the tree's is one fitted model, you are comparing a committee to an individual. Ensemble both or neither.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Split protocol and metric.&lt;/strong&gt; Single splits on 5–50k-row tables have standard deviations large enough to swallow every effect size in this debate: use repeated CV, multiple outer seeds, confidence intervals on everything. If the intervals overlap you have a preference, not a result. And drop accuracy — score AUC-PR on imbalanced problems, and log loss or Brier plus a reliability curve wherever downstream decisions consume the probabilities. Trees and pretrained transformers calibrate differently, and a better-ranking, worse-calibrated model can win your benchmark while costing you money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pretraining contamination.&lt;/strong&gt; The most under-checked failure of the new regime. For a model pretrained on a large corpus of tables, "zero-shot performance on a public dataset" is a claim about leakage as much as about generalization — the forecasting community built explicit non-leaking splits into GIFT-Eval and now flags leaking models on the board precisely because this bit them. If you are benchmarking on &lt;code&gt;adult&lt;/code&gt; or &lt;code&gt;covertype&lt;/code&gt;, you are not measuring what you think. Evaluate on &lt;em&gt;your&lt;/em&gt; data; it is the only evaluation that answers your actual question anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost as a first-class axis.&lt;/strong&gt; Plot accuracy against serving cost. The numbers that matter are training time per 1,000 samples, prediction latency per row at your concurrency, and whether inference needs a GPU. On published TabArena timings foundation models are far cheaper to &lt;em&gt;train&lt;/em&gt; than a tuned tree sweep, and trees are far cheaper to &lt;em&gt;serve&lt;/em&gt; — which dominates depends on your retraining cadence and request volume, and no leaderboard can tell you that.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where the gap comes from.&lt;/strong&gt; Aggregate Elo hides the mechanism, and the mechanism is documented: the advantage concentrates under data scarcity and complex interactions, and erodes or reverses on large, predominantly numeric tables. Pretrained models bring learned priors over table structure; boosting extracts dataset-specific signal iteratively. Given enough rows, the boosting exhausts the available signal and the prior stops adding anything. Benchmark work keeps converging on the same conclusion — performance is driven by &lt;em&gt;feature structure&lt;/em&gt;, not model complexity or row count — and CatBoost remains the safest pick when categoricals dominate.&lt;/p&gt;
&lt;p&gt;&lt;img alt="DeltaW" src="/images/baselining-tabular-2026.png"&gt;&lt;/p&gt;
&lt;h3&gt;A protocol worth actually running&lt;/h3&gt;
&lt;p&gt;If you want a defensible answer for your own problem, this takes an afternoon:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fix the harness before you look at any model.&lt;/strong&gt; Repeated stratified CV, ≥5 outer seeds, metrics derived from the downstream decision, confidence intervals mandatory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Establish the floor.&lt;/strong&gt; Logistic regression or a default random forest on your real preprocessing. Skipping this is how teams celebrate a foundation model that beat a broken pipeline — and a surprising number of "hard" problems turn out to be linear-plus-noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Establish the reference.&lt;/strong&gt; LightGBM or CatBoost: defaults, then a fixed wall-clock tuning budget, then tuned-plus-ensembled. Three numbers, not one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the zero-shot challenger.&lt;/strong&gt; One forward pass from a tabular foundation model. Nearly free, and under ~10k rows it will often win outright.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plot the frontier, don't rank the models.&lt;/strong&gt; Metric against serving cost, then pick the point that fits your constraints rather than the top of a list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Score the non-quantitative axes explicitly.&lt;/strong&gt; Auditability, drift behaviour, retraining ergonomics, vendor dependency, deployability. Write them down as requirements before the numbers seduce you.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Step 6 is where foundation model pilots quietly die. In regulated settings — credit scoring, underwriting, clinical decision support — a documented GBDT with monotonic constraints, stable attributions, and a reproducible training run clears model risk review. A third-party pretrained transformer with opaque failure modes and a shifting version history is a much harder conversation, and you will have it again every time the vendor ships a checkpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Time series is a genuinely different story&lt;/h2&gt;
&lt;p&gt;The tabular argument and the forecasting argument get conflated constantly, and they should not be. In forecasting, the split is not old-versus-new — it is about whether you have history.&lt;/p&gt;
&lt;p&gt;For true &lt;strong&gt;zero-shot&lt;/strong&gt; forecasting, pretrained models have a structural advantage that trees cannot replicate. Chronos, TimesFM, Moirai-2, Sundial, and TabPFN-TS all forecast a series they have never seen. Moirai-2 ranks near the top of GIFT-Eval among non-leaking models, trained on tens of billions of observations across many domains. If you have forty thousand new series with no usable history and need something serving tomorrow, that is where you start, and it isn't close.&lt;/p&gt;
&lt;p&gt;But when you have sufficient in-domain history, exogenous covariates — prices, promotions, weather, calendar effects, holidays — and a hierarchy to reconcile, feature-engineered gradient boosting remains competitive and often better. The M5 pattern (one global GBDT over lag and calendar features across all series) has not been dethroned by anything for that class of problem. One striking data point: when Google DeepMind pointed an automated code-search system at GIFT-Eval, it outperformed the entire leaderboard — foundation models included — and the solutions it discovered converged strongly on gradient boosting and ensemble/decomposition methods. An unbiased search over the space of forecasting approaches walked toward boosting.&lt;/p&gt;
&lt;p&gt;Recent work frames this as a break-even analysis rather than a ranking, which is the right frame: foundation models need GPU infrastructure, have opaque failure modes, and can underperform on highly domain-specific data, while boosting and classical smoothing methods are interpretable, run on CPUs, and carry decades of empirical validation. The break-even point is essentially a function of how much relevant history you have per series.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The constraint nobody budgeted for&lt;/h2&gt;
&lt;p&gt;There is an unglamorous reason this matters more in 2026 than it would have in 2024: GPUs are not a commodity you can assume.&lt;/p&gt;
&lt;p&gt;The 2026 crunch is not one shortage but a stack of them — HBM, CoWoS packaging, wafer capacity, data center power, cloud allocation — all tight at once. Data-center GPU lead times are reported at 36 to 52 weeks, hyperscalers have locked up much of the allocation on multi-year commitments, and teams shut out of reserved pools land on on-demand pricing at multiples of budget. In some regions the binding constraint has shifted again, to grid interconnection.&lt;/p&gt;
&lt;p&gt;This is not an argument that foundation models are bad. It is an argument that "needs a GPU at inference time" is a line item with scarcity risk attached, currently priced by the market rather than by your infra team. A model that serves at microsecond latency on CPUs you already own carries option value that appears in no accuracy table. If your churn model is a LightGBM on commodity hardware, capacity planning is solved; if it needs an accelerator per replica, you are competing for allocation against every LLM workload in your company — and losing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;So: what do you actually do&lt;/h2&gt;
&lt;p&gt;Keep a GBDT — LightGBM or CatBoost — as a mandatory baseline, permanently. Not out of nostalgia, but because it is the instrument that tells you whether your problem is hard or whether you are making it hard. It trains on a laptop, it tells you which features matter, and it will catch the leakage in your pipeline before a foundation model launders it into a suspiciously good score.&lt;/p&gt;
&lt;p&gt;Add a zero-shot tabular foundation model as a second free shot. On small and medium tables it frequently wins with no tuning and the cost of finding out is one forward pass. Treat it as a strong candidate, not a replacement.&lt;/p&gt;
&lt;p&gt;Random forests specifically occupy narrower ground than XGBoost does. They remain worth having for immediate baselining, for quantile-based uncertainty estimates, for small noisy datasets where boosting overfits, and as ensemble members. They are rarely the final answer any more.&lt;/p&gt;
&lt;p&gt;And then decide on your data, with your metric, at your cost point — not on a leaderboard. The frontier is moving fast enough that any specific recommendation here has a shelf life of about two quarters. The evaluation discipline does not.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;TabArena: A Living Benchmark for Machine Learning on Tabular Data — https://arxiv.org/pdf/2506.16791 · live board: https://tabarena.ai&lt;/li&gt;
&lt;li&gt;TabPFN-3 Technical Report (leaderboard tables, timings) — https://arxiv.org/pdf/2605.13986&lt;/li&gt;
&lt;li&gt;Hollmann et al., "Accurate predictions on small data with a tabular foundation model," &lt;em&gt;Nature&lt;/em&gt; 637&lt;/li&gt;
&lt;li&gt;Google TabFM release coverage and the ensembling caveat — https://ai-crucible.com/articles/tabfm-ensembling-wins-even-for-tables/&lt;/li&gt;
&lt;li&gt;Tabular model benchmark across 19 datasets (feature-structure findings) — https://aimultiple.com/tabular-models&lt;/li&gt;
&lt;li&gt;GIFT-Eval: A Benchmark for General Time Series Forecasting Model Evaluation — https://arxiv.org/abs/2410.10393&lt;/li&gt;
&lt;li&gt;"When Do Foundation Models Pay Off? A Break-Even Analysis of Pretrained Time Series Forecasters" — https://arxiv.org/html/2607.04919v1&lt;/li&gt;
&lt;li&gt;"An AI system to help scientists write expert-level empirical software" (GIFT-Eval result converging on gradient boosting) — https://arxiv.org/pdf/2509.06503&lt;/li&gt;
&lt;li&gt;Schmitt, "Deep Learning vs. Gradient Boosting: benchmarking for credit scoring" — https://arxiv.org/pdf/2205.10535&lt;/li&gt;
&lt;li&gt;GPU and compute supply constraints, 2026 — https://www.apollo.com/wealth/insights-news/insights/2026/06/growing-compute-shortage · https://vexxhost.com/blog/gpu-capacity-crisis-ai-infrastructure-2026/ · https://inflect.com/blog/data-center-power-shortage-2026-why-grid-capacity-is-now-the-bigger-constraint-than-gpus&lt;/li&gt;
&lt;/ul&gt;</content><category term="AI"/><category term="ml"/><category term="tabular"/><category term="foundation-models"/><category term="xgboost"/><category term="forecasting"/><category term="benchmarks"/><category term="gpu"/></entry><entry><title>Hardware Ate Software. But the Survivors Become Agents</title><link href="https://blog.r6i.it/hardware-ate-software-survivors-become-agents.html" rel="alternate"/><published>2026-07-14T09:30:00+02:00</published><updated>2026-07-14T09:30:00+02:00</updated><author><name>Sam Reghenzi</name></author><id>tag:blog.r6i.it,2026-07-14:/hardware-ate-software-survivors-become-agents.html</id><summary type="html">&lt;p&gt;A response to "Software Ate the World, Now Hardware Is Eating Software." The diagnosis is right — value is migrating and point software is being crushed. The prognosis is wrong. When the bottom of the stack commoditizes, value doesn't stay there. It climbs back up to the layer the thesis forgot: the agent.&lt;/p&gt;</summary><content type="html">&lt;p&gt;There's a piece going around called &lt;a href="https://www.datagravity.dev/p/software-ate-the-world-now-hardware"&gt;&lt;em&gt;Software Ate the World. Now Hardware Is Eating Software&lt;/em&gt;&lt;/a&gt;, and it is good enough that I want to argue with it. The thesis is that the center of economic gravity in tech is sliding &lt;em&gt;down&lt;/em&gt; the stack — toward the layers with the deepest physical constraints and the strongest control points: semiconductors, data platforms, and the inference engines that run open models. NVIDIA at ~75% gross margins. Hyperscaler capex heading toward $5.3 trillion by 2030. Application margins compressing from the 75-90% of the SaaS era down to 50-60%. The conclusion: stop betting on apps, start betting on silicon and data gravity.&lt;/p&gt;
&lt;p&gt;Part of what makes the argument land is that the &lt;em&gt;kind&lt;/em&gt; of hardware underneath us has changed. For twenty years the substrate was commodity: RAM, CPU, disk — parts that were cheap, interchangeable, and evolving slowly enough that nobody thought about them. You provisioned them and forgot them. The AI stack runs on GPUs, and GPUs are a different animal entirely: faster-moving on the evolutionary curve, far more expensive, and carrying a margin the commodity components never did. When the foundation stops being fungible and starts being a scarce, high-margin, fast-obsolescing asset, it's natural to conclude that this is where the money now lives. That shift is real, and it's the strongest part of the article.&lt;/p&gt;
&lt;p&gt;I agree with almost every number. I disagree with where the arrow points.&lt;/p&gt;
&lt;h2&gt;What the article gets right&lt;/h2&gt;
&lt;p&gt;Let me not build a strawman. The diagnosis is correct in two important ways.&lt;/p&gt;
&lt;p&gt;First, value &lt;em&gt;is&lt;/em&gt; migrating. The comfortable two-decade assumption that the application layer is where money accretes is genuinely breaking. If your product is a thin wrapper over a model call, your margin is a rounding error on someone else's capex.&lt;/p&gt;
&lt;p&gt;Second — and this is the part people flinch at — &lt;strong&gt;point software is condemned.&lt;/strong&gt; The single-purpose tool, the app that does one thing behind a form and a database, is being squeezed exactly as the article says. That compression from 90% to 50% margins is real, and it isn't a blip. So far, so aligned.&lt;/p&gt;
&lt;p&gt;Where I get off the train is the prognosis. The article measures where the &lt;em&gt;capital&lt;/em&gt; is going and assumes that's where the &lt;em&gt;value&lt;/em&gt; will be captured. Those are not the same thing.&lt;/p&gt;
&lt;h2&gt;Capital intensity is not value capture&lt;/h2&gt;
&lt;p&gt;The whole argument rests on a slide from "this layer is expensive and physically constrained" to "this layer captures the value." But capital intensity and value capture are different axes. Railroads were brutally capital-intensive; most of the durable value ended up with the businesses that &lt;em&gt;ran on top of the rails&lt;/em&gt;, not the ones that laid them. Being hard to build is not the same as being where the margin lives.&lt;/p&gt;
&lt;p&gt;Here's the move the article misses: &lt;strong&gt;commoditization at the bottom doesn't destroy value, it relocates it.&lt;/strong&gt; When a layer gets cheap and abundant, the barrier drops and value floods into whatever sits on top and can now do something new.&lt;/p&gt;
&lt;p&gt;I keep bumping into this in practice, not in theory. A few months ago I &lt;a href="/running-gemma-4-31b-on-an-apple-silicon-mac-with-ollama.html"&gt;ran Gemma 4 31B on a 32GB Apple Silicon Mac with Ollama&lt;/a&gt;. No tokens, no API bill, no hyperscaler in the loop. The article would file that under "inference commoditizing, margins compressing." Fine. But look at what commodity inference actually &lt;em&gt;did&lt;/em&gt;: it put a capable model on my desk and collapsed the cost of building something on top of it to roughly zero. That is not value destruction. That is the barrier falling — and value doesn't evaporate when a barrier falls, it moves to whoever is standing on the other side of it.&lt;/p&gt;
&lt;p&gt;The open-vs-closed gap closing from 8% to 1.7% in a year is the same story told as a threat. The article reads it as "models are commoditizing, so the value sinks to whoever owns the compute." I read it as "the intelligence layer just became a utility, so the interesting question is what you &lt;em&gt;build&lt;/em&gt; with a utility."&lt;/p&gt;
&lt;h2&gt;The layer the thesis forgot&lt;/h2&gt;
&lt;p&gt;The article has five layers: semiconductors, open models and inference, premium models, data platforms, applications. Notice what's missing. There is no layer for the thing that turns a commodity model into something that does work in the world. No orchestration. No integration. No agent.&lt;/p&gt;
&lt;p&gt;That omission is the whole disagreement in one gap.&lt;/p&gt;
&lt;p&gt;Because the honest thing to say about point software is not that it's dying — it's that it's &lt;em&gt;metamorphosing&lt;/em&gt;. The app stops being a static interface you operate and becomes an agent that operates on your behalf. Features stop being screens and become capabilities the agent can reach for. The value was never in the form and the database; it was in the judgment about what to do next, and that judgment is exactly what an agent embodies.&lt;/p&gt;
&lt;p&gt;This is not a slide deck abstraction for me. It's most of what I've been writing about. The reason I keep picking at &lt;a href="/skills-vs-mcp-tokens-security.html"&gt;MCP versus skills&lt;/a&gt;, or at &lt;a href="/oauth-original-sin-mcp-auth.html"&gt;the way OAuth cracks when you hand it to a model with no eyes and no hesitation&lt;/a&gt;, is that this is the scaffolding of the agent layer. How a model reaches the outside world, what it's allowed to touch, how it holds a session — those aren't plumbing details below the value. In an agent-shaped world they &lt;em&gt;are&lt;/em&gt; the value.&lt;/p&gt;
&lt;h2&gt;Where gravity actually climbs&lt;/h2&gt;
&lt;p&gt;So if the bottom of the stack is commoditizing — and it is — where does the gravity go?&lt;/p&gt;
&lt;p&gt;Not the GPUs. You can rent those by the hour, and the whole thrust of open models and local inference is to make the raw intelligence a fungible input. Owning silicon is owning the railroad.&lt;/p&gt;
&lt;p&gt;The new control points are further up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; — the logic that decides which capability to invoke, in what order, and how to recover when a step fails. This is genuinely hard and genuinely defensible, and it lives nowhere in the five-layer map.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proprietary data loops&lt;/strong&gt; — the article actually gets this half-right under "data gravity," but frames it as a moat for &lt;em&gt;platforms&lt;/em&gt; rather than the fuel that makes an &lt;em&gt;agent&lt;/em&gt; good at your specific problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent reliability and safety&lt;/strong&gt; — the boring, unglamorous work of making an autonomous thing trustworthy enough to hand real permissions. Whoever solves this owns a lock-in that no amount of capex buys.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are cheap in capital. They're expensive in judgment, in data, and in the accumulated scar tissue of making agents behave. That's a different kind of moat than a fab, and it accrues to a different kind of builder.&lt;/p&gt;
&lt;h2&gt;The engineer's version of the thesis&lt;/h2&gt;
&lt;p&gt;The article is written for an investor: it's a map of where to allocate &lt;em&gt;capital&lt;/em&gt; across a stack. Read it as an engineer and the question changes — not "where do I put my money" but "where do I put my effort." And once you flip that lens, almost every conclusion inverts with it.&lt;/p&gt;
&lt;p&gt;If you believe — as I do, and as &lt;a href="https://www.youtube.com/watch?v=83fWzQSWB10"&gt;the argument that software becomes agents rather than standalone tools&lt;/a&gt; lays out — that every app worth keeping is going to become an agent, then the commoditization of hardware and inference isn't the headline. It's the &lt;em&gt;precondition&lt;/em&gt;. Cheap, abundant, ownable intelligence is the raw material. The investor sees a low-margin commodity and walks away. The engineer sees a free input and asks what can finally be built now that it costs nothing. Those are opposite reactions to the same fact, and only one of them ships anything.&lt;/p&gt;
&lt;p&gt;So let me be concrete about what building in an agent-shaped world actually looks like, because "software becomes agents" is easy to say and easy to get wrong.&lt;/p&gt;
&lt;h3&gt;The interface inverts&lt;/h3&gt;
&lt;p&gt;The oldest assumption in application software is that a human drives and the software responds. You click, it reacts. Every screen, every form, every menu is scaffolding for a person operating a machine. The agent inverts this: the software drives, and the human sets intent and supervises. That is not a UI refresh — it's a different center of gravity for the whole system. The screen stops being the product. The product is the loop of &lt;em&gt;perceive → decide → act → check&lt;/em&gt;, and the screen is just one place a human can peek into it.&lt;/p&gt;
&lt;p&gt;Practically, this means the work you used to pour into the front end — the state management, the wizard flows, the careful choreography of what the user can click next — largely evaporates or moves. What replaces it is the design of the agent's &lt;strong&gt;capability surface&lt;/strong&gt;: the set of actions it can take, described well enough that a model can choose between them correctly, with the guardrails that keep a wrong choice from being catastrophic. That is engineering work, and it's harder than the front end it replaces, because your "user" now has no eyes, no common sense, and no hesitation.&lt;/p&gt;
&lt;h3&gt;Features stop being screens and become tools&lt;/h3&gt;
&lt;p&gt;In the old model, a feature is something you build &lt;em&gt;and then teach a human to find&lt;/em&gt;: a button, a settings page, a documented workflow. In the agent model, a feature is a &lt;strong&gt;tool the agent can reach for&lt;/strong&gt; — and the entire discipline of exposing tools to a model is exactly the terrain I keep writing about. Whether you reach for &lt;a href="/skills-vs-mcp-tokens-security.html"&gt;MCP or a plain CLI skill&lt;/a&gt; is not a plumbing decision underneath the value. It &lt;em&gt;is&lt;/em&gt; the product surface. Get the tool descriptions wrong and the smartest model in the world invokes the wrong capability at the wrong time. Get the permission boundaries wrong and you've handed &lt;a href="/oauth-original-sin-mcp-auth.html"&gt;an OAuth token to something with no judgment about when to use it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is where the article's five-layer map fails an engineer most concretely. There is no line item for "the contract between the agent and the world," and yet that contract is where most of the real engineering — and most of the real defensibility — now lives.&lt;/p&gt;
&lt;h3&gt;What you actually invest in&lt;/h3&gt;
&lt;p&gt;If effort is the currency, here is where I'd spend it, in order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The capability layer.&lt;/strong&gt; The tools, their descriptions, their boundaries, their failure modes. This is the new API design, and it's the highest-leverage code you'll write.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The data loop.&lt;/strong&gt; Not "data" as a static asset the way the article frames data gravity, but the &lt;em&gt;loop&lt;/em&gt;: the agent acts, the outcome is captured, and that outcome makes the next action better. An agent with a tight loop on your specific problem beats a bigger model with none. This is a moat you build, not one you buy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability and evals.&lt;/strong&gt; The unglamorous scaffolding that tells you whether the agent is getting better or quietly regressing. In an agent-shaped product, your test suite is a behavioral one, and building it is most of the job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model-agnosticism.&lt;/strong&gt; Precisely &lt;em&gt;because&lt;/em&gt; the intelligence layer is commoditizing, you should treat the model as a swappable part. The day I &lt;a href="/running-gemma-4-31b-on-an-apple-silicon-mac-with-ollama.html"&gt;ran Gemma on a Mac&lt;/a&gt; I wasn't proving a Mac can host a model — I was proving the model is a component I can source three different ways. Build so that swapping it is a config change, not a rewrite, and the commoditization the article mourns becomes leverage you own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; on that list: owning the silicon, training a frontier model, betting the company on a fab. Those are capital plays, and they belong to people with capital. The engineer's plays are all further up — and they compound with judgment and data, which is the one kind of moat that gets &lt;em&gt;deeper&lt;/em&gt; the cheaper the hardware underneath it gets.&lt;/p&gt;
&lt;h3&gt;The product is the job&lt;/h3&gt;
&lt;p&gt;Everything above is about &lt;em&gt;how&lt;/em&gt; you build. But the sharpest way to feel the shift is to look at &lt;em&gt;what you sell&lt;/em&gt;. SaaS sells software: a tool your customer's team logs into and operates. Agent SaaS sells &lt;strong&gt;work&lt;/strong&gt;: you take a job the customer's team currently does, you package it whole, and you sell the outcome as a service. The team stops operating a tool and starts handing off a responsibility.&lt;/p&gt;
&lt;p&gt;It sounds like a small rewording. It changes everything about how both sides think. The buyer stops evaluating features and seat counts and starts asking a much simpler question — "will this reliably get the job done?" — the same question they'd ask when hiring. And the builder stops shipping capabilities and hoping the customer assembles them into value; you're now on the hook for the value itself. That's a heavier promise, and it's exactly why the margin follows it back up the stack. Nobody pays SaaS-commodity prices for something that shows up, does the work, and is accountable for the result.&lt;/p&gt;
&lt;p&gt;This is also why the commoditization the article mourns is a tailwind here, not a threat. When the intelligence underneath is cheap and abundant, the cost of &lt;em&gt;doing the job&lt;/em&gt; collapses — and the price you can charge is anchored to the human labor you're replacing, not to your compute bill. The gap between those two numbers is the whole business.&lt;/p&gt;
&lt;h3&gt;What this looks like in the wild&lt;/h3&gt;
&lt;p&gt;The pattern is already shipping, and the tell is that these products don't describe themselves as software at all — they describe themselves as a role.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.slang.ai/"&gt;Slang AI&lt;/a&gt; sells an "AI super host" for restaurants. It answers the phone, manages reservations, routes VIPs to a human, and pings staff when something needs attention. Nobody at the restaurant logs into a dashboard to "use the reservation feature" — the phone just gets answered, all of it, every time. The product &lt;em&gt;is&lt;/em&gt; the host.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.sameday.ai/"&gt;Same Day&lt;/a&gt; does the equivalent for home services: AI dispatchers and receptionists that answer calls, book jobs, and reschedule without a human touching them. Again, the sale isn't a tool — it's the front desk.&lt;/p&gt;
&lt;p&gt;What makes both work is the bar they clear: handle one genuinely annoying job &lt;strong&gt;better than a junior hire, faster than an agency, and cheaper than new headcount&lt;/strong&gt;. That's the whole value proposition, and notice it's measured against &lt;em&gt;people&lt;/em&gt;, not against other software. That's the giveaway that these aren't apps anymore. You don't benchmark a SaaS tool against a receptionist. You benchmark an agent against one — because it's doing the receptionist's job.&lt;/p&gt;
&lt;h3&gt;The prognosis, flipped&lt;/h3&gt;
&lt;p&gt;So yes: software ate the world, and now hardware is eating software. But something always crawls out of the thing that got eaten. This time it's the agent — and it's standing on all that cheap silicon, &lt;em&gt;using&lt;/em&gt; it, not being replaced by it. The article looked down the stack and saw where the capital sinks. Look up the stack and you see where the work goes, and the work is where builders have always eventually captured the value.&lt;/p&gt;
&lt;p&gt;The point software is dying. Long live the agent.&lt;/p&gt;</content><category term="AI"/><category term="llm"/><category term="agents"/><category term="hardware"/><category term="inference"/><category term="mcp"/><category term="economics"/><category term="stack"/></entry><entry><title>OAuth's Original Sin: Why MCP Inherited a Problem Designed for Humans</title><link href="https://blog.r6i.it/oauth-original-sin-mcp-auth.html" rel="alternate"/><published>2026-06-19T22:48:00+02:00</published><updated>2026-06-19T22:48:00+02:00</updated><author><name>Sam Reghenzi</name></author><id>tag:blog.r6i.it,2026-06-19:/oauth-original-sin-mcp-auth.html</id><summary type="html">&lt;p&gt;The third post in the MCP series. The first two argued &lt;em&gt;when&lt;/em&gt; MCP wins and loses. This one looks at the terrain MCP is supposed to win on — OAuth-bound integrations — and shows why even there, the ground is more fragile than it looks.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Two posts ago I drew what felt like a clean line: MCP earns its keep when text and shell are genuinely insufficient — and one of the canonical examples I gave was "holding an OAuth session." It still is. But it is also the case I most undersold. Because OAuth, the protocol now standing under every serious MCP integration, was not designed for what we are about to do to it.&lt;/p&gt;
&lt;p&gt;OAuth was designed for a human, sitting at a keyboard, reading a consent screen, clicking &lt;em&gt;Allow&lt;/em&gt;. Every layer of the protocol — the browser redirect, the authorization code that crosses domains, the refresh token that lives on a device the user controls — presumes that human in the loop. MCP quietly removes them. What remains is the protocol, the tokens, and a language model holding both.&lt;/p&gt;
&lt;p&gt;This post is about what happens when you take a security architecture designed for a conscious human and run it under software that has neither eyes nor hesitation.&lt;/p&gt;
&lt;h2&gt;The protocol that assumed a human&lt;/h2&gt;
&lt;p&gt;It is worth saying out loud, because the assumption is so deep we no longer notice it: OAuth is a protocol whose defensive mechanisms only work because a person is paying attention.&lt;/p&gt;
&lt;p&gt;The standard story names three actors — resource owner, client, resource server — but the protocol's whole defensive posture is built around a fourth, implicit actor: a conscious human in front of the client. The redirect URI matching only matters because someone might notice the URL bar is wrong. The consent screen only works because someone reads "this app would like to access your calendar" and decides whether that is acceptable. The scope display only protects you if you scan the list. Even refresh tokens were designed with the unstated assumption that long-lived background activity would be rare, deliberate, and visible to the user whose token it was.&lt;/p&gt;
&lt;p&gt;Take that human out, and OAuth still &lt;em&gt;runs&lt;/em&gt; — but most of what made it safe stops working.&lt;/p&gt;
&lt;p&gt;Now name what removed them. Not malware, not a clever new attack class. An interface change. We replaced "person, browser, consent screen" with "agent, MCP client, system prompt." The protocol below didn't change. The contract above did.&lt;/p&gt;
&lt;h2&gt;What MCP inherited&lt;/h2&gt;
&lt;p&gt;To MCP's credit, it inherited the best version of the contract available. The current authorization spec — building on the November 2025 revision — rests on &lt;strong&gt;OAuth 2.1&lt;/strong&gt; with mandatory PKCE, &lt;strong&gt;RFC 9728&lt;/strong&gt; for Protected Resource Metadata, and &lt;strong&gt;RFC 8414&lt;/strong&gt; for Authorization Server Metadata. Implicit grant is banned. Plain PKCE is banned. Bearer tokens in URLs are banned. Refresh tokens must be rotated or sender-constrained. Redirect URIs require exact-string matching. The June 2025 revision pulled token issuance out of MCP servers entirely and re-modelled them as OAuth &lt;em&gt;resource servers&lt;/em&gt;, deferring identity to external providers where it belongs.&lt;/p&gt;
&lt;p&gt;These are good choices. None of them, however, addresses the new asymmetry. They address the &lt;em&gt;old&lt;/em&gt; failures of OAuth — the ones discovered when humans were still at the controls. The MCP spec doesn't yet have a vocabulary for the failures introduced by removing the human, because the protocol underneath doesn't either. What it has is patches, applied as those failures surface.&lt;/p&gt;
&lt;h2&gt;The cracks: four assumptions that break&lt;/h2&gt;
&lt;p&gt;If you look at the MCP-OAuth interface as a system of assumptions, four of them stop being true the moment an LLM agent sits where the human used to. None of these is theoretical; each maps to incidents already in the wild.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consent comprehension.&lt;/strong&gt; The user signs a consent screen listing scopes. They never see the &lt;em&gt;superset&lt;/em&gt; of tools the agent might invoke during the session — and they couldn't, because the superset is constructed dynamically by the agent's reasoning, not by the consent UI. The consent screen was designed to summarise a &lt;em&gt;page&lt;/em&gt;. It was never designed to summarise an &lt;em&gt;agent's reasoning surface&lt;/em&gt;. The user clicks &lt;em&gt;Allow&lt;/em&gt; on a list of capabilities; the agent treats &lt;em&gt;Allow&lt;/em&gt; as permission to &lt;em&gt;compose&lt;/em&gt; those capabilities in arbitrary ways the user never anticipated. The same scopes the user understood on the consent screen mean something different by the time the agent uses them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token custody.&lt;/strong&gt; In the browser model, the access token lives in cookie storage, a keychain, a secure enclave — somewhere the user controls and can wipe. In the agent model, the access token lives, at least during use, &lt;em&gt;inside the model's context window&lt;/em&gt;. Which is to say: in the same channel that can be prompt-injected. The token is co-resident with untrusted input. This is not a hypothetical: Supabase's Cursor integration was demonstrated in 2025 to be exfiltratable via prompt-injected support tickets that instructed the agent to surface its own credentials. The token wasn't stolen by a network attacker; it was &lt;em&gt;asked for&lt;/em&gt; by the data the agent was processing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope locality.&lt;/strong&gt; A human uses a token on the site they authorised it for. They opened the tab; they will close the tab; the token's relevance is bounded by the user's attention. An agent uses tokens wherever they help solve the task. Cross-tool reasoning means an MCP server's token may be referenced, summarised, or — most worryingly — &lt;em&gt;paraphrased into other tool calls&lt;/em&gt; without anyone explicitly composing those flows. OAuth's mental model assumes a token stays where it was granted. Agents do not respect that geometry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session presence.&lt;/strong&gt; Humans close tabs. Agents do not. Refresh tokens were designed with the assumption that long-lived background activity is rare and visible. With agents, long-lived background activity is the default, and visibility depends entirely on logging the user may or may not have set up. The implicit dead-man's switch of "the user got bored and walked away" is gone.&lt;/p&gt;
&lt;p&gt;These four cracks are the post's load-bearing argument. Everything that follows — the spec's evolution, the real attacks, the open questions — is illustration.&lt;/p&gt;
&lt;h2&gt;Zoom-in 1: anatomy of a real one-click takeover&lt;/h2&gt;
&lt;p&gt;The cleanest illustration of how these assumptions fail in production is the work disclosed by Obsidian Security between July and August 2025, with patches landing through September.&lt;/p&gt;
&lt;p&gt;&lt;a href="images/oauth-mcp-takeover.png" title="Click to enlarge"&gt;&lt;img src="images/oauth-mcp-takeover.png" alt="One-click account takeover: broken vs. fixed OAuth flow" width="520"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The pattern was depressingly common. Several well-known remote MCP servers had been implemented as &lt;strong&gt;OAuth proxies&lt;/strong&gt;: the MCP server stood between the user's client and an upstream SaaS authorisation server (a productivity tool, a developer platform, a comms app), forwarding the OAuth dance and brokering tokens. To talk upstream, the MCP proxy used a single static &lt;code&gt;client_id&lt;/code&gt; — &lt;em&gt;shared across all users of that MCP server&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Three additional design choices, each defensible in isolation, combined into a one-click takeover:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The shared &lt;code&gt;client_id&lt;/code&gt; meant the upstream auth server, once any user had ever consented, treated subsequent authorisation requests as already-approved. No fresh consent prompt; just a returned code.&lt;/li&gt;
&lt;li&gt;The MCP proxy's &lt;code&gt;redirect_uri&lt;/code&gt; matching was loose — substring or prefix, not exact — and accepted callback URLs the attacker could control.&lt;/li&gt;
&lt;li&gt;The OAuth &lt;code&gt;state&lt;/code&gt; parameter, whose entire purpose is to bind a callback to the user session that initiated it, was not strictly bound to that session.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The attack was a single crafted link. The victim clicks. The MCP proxy initiates the upstream OAuth dance. The upstream server, recognising the shared &lt;code&gt;client_id&lt;/code&gt; as already-consented, returns an authorisation code without a prompt. The MCP proxy redirects to a URL that &lt;em&gt;looks&lt;/em&gt; like one of its own but actually routes to the attacker. The attacker exchanges the code. One click. One account.&lt;/p&gt;
&lt;p&gt;Read carefully, this is not a story about sloppy vendors. Each of those design choices made sense in a world where consent screens, redirect URI matching, and &lt;code&gt;state&lt;/code&gt; were doing the &lt;em&gt;human-supervised&lt;/em&gt; work they had always done. It is a story about a protocol whose safety mechanisms degrade in silence when the human is removed.&lt;/p&gt;
&lt;p&gt;The November 2025 spec update introduced &lt;strong&gt;CIMD&lt;/strong&gt; (Client Instance Metadata Document) precisely so each client instance can be uniquely identified at the protocol level, ending the shared-&lt;code&gt;client_id&lt;/code&gt; failure mode at the source. It also tightened guidance on redirect URI matching and session-bound &lt;code&gt;state&lt;/code&gt;. The patch is good. What it patches is not a bug; it is a category of assumption.&lt;/p&gt;
&lt;h2&gt;Zoom-in 2: the DCR dilemma&lt;/h2&gt;
&lt;p&gt;If the takeover story is the &lt;em&gt;sharp&lt;/em&gt; edge of MCP-OAuth, the second zoom-in is the &lt;em&gt;blunt&lt;/em&gt; one — a tension that has no resolution, only positions on a spectrum.&lt;/p&gt;
&lt;p&gt;&lt;a href="images/dcr-spectrum.png" title="Click to enlarge"&gt;&lt;img src="images/dcr-spectrum.png" alt="The DCR spectrum: convenience vs. control" width="520"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dynamic Client Registration&lt;/strong&gt; (RFC 7591) lets a client register itself with an authorisation server at runtime, receiving a &lt;code&gt;client_id&lt;/code&gt; and credentials without human paperwork. For agents, this is almost a necessity: agents spawn ephemerally — in dev environments, in CI, in user sessions, in transient containers — and pre-registering each instance does not scale. The MCP spec permits DCR for exactly this reason.&lt;/p&gt;
&lt;p&gt;Most enterprises &lt;strong&gt;block DCR&lt;/strong&gt;. Also for good reason: anyone who can reach a registration endpoint can mint a client and start requesting tokens. In a world of registration flooding and client impersonation, the simplest defensive posture is "no one registers a client we did not approve in advance."&lt;/p&gt;
&lt;p&gt;Both positions are correct. They are correct about &lt;em&gt;different things&lt;/em&gt;. The agent-friendly position is correct about how the world actually wants to compose ephemeral software. The enterprise position is correct about how identity surface area becomes attack surface. The spec cannot tell either of them they are wrong.&lt;/p&gt;
&lt;p&gt;What the spec &lt;em&gt;did&lt;/em&gt;, in November 2025, was step back. DCR moved from "SHOULD-implement" toward "optional, with explicit guidance for gated deployments" — software statements, redirect domain allowlists, IP-restricted registration endpoints. This is not the protocol picking a winner. It is the protocol acknowledging that no answer is right for every deployment, and codifying the seam where the trade-off lives.&lt;/p&gt;
&lt;p&gt;I find this the most honest moment in the MCP auth spec's short life. Specifications normally pretend the world has one answer. This one admits the world has a dial, and tells you where the dial is. That is a sign of a community learning in public.&lt;/p&gt;
&lt;h2&gt;What's still open&lt;/h2&gt;
&lt;p&gt;The November 2025 spec closed the worst-known leaks. The deeper question is unsolved, and it is older than MCP: &lt;strong&gt;what is an agent's identity?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not the client software — one client may serve many sessions, many users, many models. Not the user — the user is the resource owner; the agent acts on their behalf but is not them. Not the model — models change between turns; multiple models may chain on a single task. None of OAuth's existing slots fit. Work like the Agent Identity Protocol (arXiv 2603.24775) is starting to take the shape of the question seriously, and the field broadly recognises that something between "machine identity" and "delegated user identity" is missing from the standard kit.&lt;/p&gt;
&lt;p&gt;Until that work matures, MCP auth is doing what every protocol does when the world outruns it: bolting on constraints where the missing concept used to live. CIMD is one such bolt. The DCR downgrade is another. So is the resource-server reframing of June 2025. Each fix is a sentence that ends with "…because there is no human here anymore."&lt;/p&gt;
&lt;p&gt;The thing I keep coming back to, when I read these spec revisions side by side, is how much of what made OAuth feel safe was never actually written down. For something like twenty years the protocol quietly relied on a kind of partnership with the person sitting in front of the browser. They were the one who noticed when a URL didn't look right, who paused in front of a consent screen and asked themselves whether the application on the other side was really the one they had meant to visit, who eventually closed the tab and turned a long-running session into a finished one. None of this was in the RFCs, and yet a surprising amount of the protocol's real-world safety lived there.&lt;/p&gt;
&lt;p&gt;What MCP has done, almost as a side effect of solving a quite different problem, is dissolve that partnership. The parts of OAuth that were always specified — the flows, the cryptography, the metadata documents — are still there, working about as well as they ever did. It is the unspecified part we have lost, and we now find ourselves in the slightly awkward position of trying to write down, in protocol form, what used to be carried by attention.&lt;/p&gt;
&lt;p&gt;I find this an interesting kind of work to watch. The November 2025 revision, with its per-instance client identities, its more careful handling of redirect URIs, and its quiet downgrading of dynamic client registration, is doing something specifications rarely do well: admitting that earlier choices had encoded assumptions the world has since invalidated. Some of these new mechanical constraints will hold up. Others, I suspect, will turn out to be patches over the same shape of problem in a new place. We will know which as the next round of disclosures comes through, and the spec will move again.&lt;/p&gt;
&lt;p&gt;For those of us building on MCP today, I do not think the right response is either alarm or complacency. It is, more or less, what one would want to bring to any protocol still finding its shape: use it where it earns its keep, keep half an eye on where the standard is changing and why, and remember that the cracks will tend to reappear in the same place. They will reappear, in my experience, wherever we are still designing — without quite noticing it — for a person who is no longer there.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is the third in a series on the engineering reality of MCP. The first two (&lt;a href="https://blog.r6i.it/mcp-vs-skills-when-to-use-which-in-professional-llm-assisted-coding.html"&gt;MCP vs Skills&lt;/a&gt;, &lt;a href="https://blog.r6i.it/skills-vs-mcp-tokens-security.html"&gt;Skills vs MCP Servers&lt;/a&gt;) argued about cost and security in tool choice. This one was always going to be the harder one — comments, corrections, and disclosure stories welcome.&lt;/em&gt;&lt;/p&gt;</content><category term="AI"/><category term="llm"/><category term="mcp"/><category term="oauth"/><category term="security"/><category term="claude"/><category term="authentication"/></entry><entry><title>Skills vs MCP Servers: The Hidden Cost of Tools in LLM Workflows</title><link href="https://blog.r6i.it/skills-vs-mcp-tokens-security.html" rel="alternate"/><published>2026-06-07T00:00:00+02:00</published><updated>2026-06-07T00:00:00+02:00</updated><author><name>Sam Reghenzi</name></author><id>tag:blog.r6i.it,2026-06-07:/skills-vs-mcp-tokens-security.html</id><summary type="html">&lt;p&gt;Why CLI-based skills are often cheaper, safer, and more ergonomic than MCP servers — and the one case where MCP genuinely wins.&lt;/p&gt;</summary><content type="html">&lt;p&gt;When you start wiring a language model into real work, you quickly discover that the choice of &lt;em&gt;how&lt;/em&gt; it reaches the outside world is not a detail. It is the single decision that drives token bills, latency, security posture, and — perhaps most underrated — how much configuration pain your team is signing up for.&lt;/p&gt;
&lt;p&gt;Two approaches dominate today: &lt;strong&gt;MCP servers&lt;/strong&gt; and &lt;strong&gt;skills&lt;/strong&gt;. They look similar on the surface (both give the model "tools"), but they sit on opposite ends of almost every meaningful axis. Understanding where each one shines is the difference between an assistant that feels like a colleague and one that feels like a very expensive intern who keeps forgetting where the kitchen is.&lt;/p&gt;
&lt;h2&gt;The token tax of MCP&lt;/h2&gt;
&lt;p&gt;An MCP server is, at its core, a contract. It exposes a list of tools, and for each tool it must declare — &lt;em&gt;upfront, at the start of every interaction&lt;/em&gt; — a precise description of what the tool does, the shape of its parameters, and the structure of the data it returns. This is what makes MCP feel so clean from a software-engineering perspective: it is strongly typed, self-documenting, schema-first.&lt;/p&gt;
&lt;p&gt;But that beauty has a price, and the price is paid in tokens.&lt;/p&gt;
&lt;p&gt;Every tool description, every parameter docstring, every return-type schema gets loaded into the context window the moment the conversation begins. The model has to "see" the whole catalogue before it can decide what to do — even if, in the end, it only uses one tool out of forty. And it pays that cost again on every turn, because the tool definitions live in the context.&lt;/p&gt;
&lt;p&gt;Connect three or four MCP servers — say a Jira server,  Slack , Github !!! — and you can easily burn ten or twenty thousand tokens &lt;em&gt;before the user has even typed a question&lt;/em&gt;. That is not an exaggeration: it is the normal operating condition of a richly integrated MCP setup. And the configuration side is rarely ergonomic either: each server wants its own JSON block, its own credentials, its own lifecycle, its own quirks about transport and authentication.&lt;/p&gt;
&lt;h2&gt;Skills work the way developers actually work&lt;/h2&gt;
&lt;p&gt;A skill, by contrast, is closer to how a human engineer approaches an unfamiliar tool. You don't read the entire &lt;code&gt;git&lt;/code&gt; manual before your first commit. You type &lt;code&gt;git --help&lt;/code&gt;, then &lt;code&gt;git commit --help&lt;/code&gt; when you need it, and you progressively build a working mental model of just the parts that matter for the job in front of you.&lt;/p&gt;
&lt;p&gt;A well-designed skill encodes exactly that posture. It tells the model: &lt;em&gt;here is a CLI, here is the entry point, here is the philosophy — explore it when you need it.&lt;/em&gt; The model then uses the existing command-line tool, discovers flags on demand, and only pays for the surface area it actually touches.&lt;/p&gt;
&lt;p&gt;The implications are significant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tokens scale with the task, not with the catalogue.&lt;/strong&gt; If the work needs three &lt;code&gt;gh&lt;/code&gt; subcommands, you pay for three subcommands. The other 197 stay invisible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The CLI already exists.&lt;/strong&gt; No server to deploy, no schema to maintain, no transport to debug. You are reusing decades of Unix engineering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition is free.&lt;/strong&gt; Pipes, redirects, &lt;code&gt;xargs&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt; — the model inherits all of it. With MCP, every composition has to be modeled as a new tool or chained call.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The security angle nobody talks about&lt;/h2&gt;
&lt;p&gt;There is a second, quieter advantage of skills over MCP that I think deserves more attention: &lt;strong&gt;security&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When a skill drives a CLI, it inherits the security model of the host operating system. The command runs as the local user, with that user's permissions, against that user's keychain, SSH agent, &lt;code&gt;~/.aws/credentials&lt;/code&gt;, &lt;code&gt;gcloud&lt;/code&gt; session, &lt;code&gt;kubectl&lt;/code&gt; context — whatever credential machinery is already in place and already audited by your IT and your habits. There is no extra surface to attack, no new long-lived token to rotate, no new daemon to keep patched.&lt;/p&gt;
&lt;p&gt;MCP servers, by their nature, want their own configuration. They often need their own copies of credentials, their own auth flows, their own way of holding state between calls. That is not inherently insecure — many MCP servers are written carefully — but it is &lt;em&gt;additional&lt;/em&gt; surface area. Every new MCP server is a new place where a secret can leak, a permission can be misconfigured, or a vulnerability can hide. A skill that wraps &lt;code&gt;aws&lt;/code&gt; or &lt;code&gt;kubectl&lt;/code&gt; adds zero new credentials to your laptop. An MCP server for AWS adds at least one.&lt;/p&gt;
&lt;p&gt;For teams that already have a mature CLI-based security story — and most engineering teams do, even if they don't think of it that way — skills give you tool access for free, on top of a model the security team has already reviewed.&lt;/p&gt;
&lt;h2&gt;A controversial case: connecting to MySQL&lt;/h2&gt;
&lt;p&gt;I want to be honest about where this argument breaks down, because it taught me something.&lt;/p&gt;
&lt;p&gt;Recently I needed an assistant to work against a MySQL database. The "skill" route was obvious: just let it use the &lt;code&gt;mysql&lt;/code&gt; CLI. It is well-known, secure, and uses the system's own credential files. Clean, ergonomic, almost no setup. I expected it to be the clear winner.&lt;/p&gt;
&lt;p&gt;It wasn't. Or at least, not unambiguously.&lt;/p&gt;
&lt;p&gt;The CLI approach turned out to be &lt;em&gt;slow&lt;/em&gt; and &lt;em&gt;token-hungry&lt;/em&gt; in a way I had not anticipated. Every query opened a new client session — connecting, authenticating, executing, tearing down. The result came back as raw text, which the model then had to re-parse on every step, often re-issuing similar queries just to confirm the shape of what it had already seen. For analytical work involving many sequential queries, this was painful.&lt;/p&gt;
&lt;p&gt;The MySQL MCP server, on the other hand, kept a &lt;strong&gt;connection pool&lt;/strong&gt; open. It pre-processed the result set into a structured form the model could consume without re-tokenising whole tables. It dramatically reduced the per-query overhead, both in latency and in tokens. For that specific workload — many small queries against the same database, in the same session — MCP was genuinely the better tool.&lt;/p&gt;
&lt;p&gt;The lesson is not "MCP wins after all." The lesson is more interesting: &lt;strong&gt;MCP is worth its overhead when it provides something the CLI fundamentally cannot — persistent state, structured pre-processing, server-side intelligence&lt;/strong&gt;. A &lt;code&gt;mysql&lt;/code&gt; CLI invocation is stateless by design; it cannot pool connections or cache schema metadata across calls. The MCP server can. That capability gap, not the schema cleanliness, is what justifies the cost.&lt;/p&gt;
&lt;p&gt;So the honest rule is: prefer skills by default, but reach for MCP when the integration &lt;em&gt;needs&lt;/em&gt; state, pooling, binary assets, OAuth dances, or any other capability that a single CLI invocation genuinely cannot offer. Not because MCP is more modern, not because the schema is prettier — because the workload demands persistence the shell cannot give you.&lt;/p&gt;
&lt;h2&gt;Where MCP is the obvious answer: complex, non-textual systems&lt;/h2&gt;
&lt;p&gt;The MySQL story is the &lt;em&gt;subtle&lt;/em&gt; case. There is also a much more obvious one, and it is worth naming explicitly: systems that simply do not live in text.&lt;/p&gt;
&lt;p&gt;Think of &lt;strong&gt;Excalidraw&lt;/strong&gt;, &lt;strong&gt;Canva&lt;/strong&gt;, &lt;strong&gt;Figma&lt;/strong&gt;, a 3D scene editor, a video timeline, a BI dashboard builder. These platforms are not just "an API with some commands" — they are spatial, visual, stateful environments. A shape on a canvas has coordinates, layers, z-order, parent groups, bindings to other shapes; a Canva design has templates, brand kits, asset libraries, page hierarchies. None of that maps cleanly to a stream of bytes flowing through a pipe.&lt;/p&gt;
&lt;p&gt;For these systems, the CLI story falls apart for reasons no amount of cleverness can fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;There is no good text representation.&lt;/strong&gt; You cannot meaningfully describe "the element 40 pixels to the right of the title, snapped to the grid, grouped with the icon" in shell flags.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State is essential.&lt;/strong&gt; The user (or the model) is working &lt;em&gt;inside&lt;/em&gt; an open document, not invoking one-shot commands. Selection, focus, undo history, currently active tool — all of this is context that needs to live somewhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary and structured assets dominate.&lt;/strong&gt; Images, fonts, embedded media, vector primitives. A CLI can shuffle files around; it cannot reason about a layout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Round-trips need to be cheap.&lt;/strong&gt; Adding a node, moving it, re-styling it, and reading back the result needs to feel like a conversation with the canvas, not a series of full re-renders.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An MCP server for Excalidraw or Canva is in its element precisely here. It can hold the document open, expose primitives that match the &lt;em&gt;concepts&lt;/em&gt; of the tool (create node, connect nodes, set fill, group selection), and let the model think in the vocabulary of the platform instead of in the vocabulary of &lt;code&gt;bash&lt;/code&gt;. The token cost of the schema, which felt extravagant for a database CLI, suddenly looks like a bargain when the alternative is "describe a whiteboard over stdout."&lt;/p&gt;
&lt;p&gt;In other words: the more a system departs from the Unix philosophy of "everything is text," the more MCP earns its keep. For canvases, design tools, creative software, and rich SaaS environments with their own native object models, MCP is not a luxury — it is the only honest way to bridge model and tool.&lt;/p&gt;
&lt;h2&gt;The epic conclusion&lt;/h2&gt;
&lt;p&gt;There is a beautiful symmetry in all this. We spent decades building Unix into one of the most quietly powerful platforms in computing: composable, scriptable, securable, observable, and — crucially — &lt;em&gt;cheap&lt;/em&gt;. Every command-line tool is a small, hard-won monument to clarity. Every credential file, every SSH key, every IAM role represents years of organisational learning about how to keep secrets in the right hands.&lt;/p&gt;
&lt;p&gt;Skills let language models stand on that mountain.&lt;/p&gt;
&lt;p&gt;They let the model behave like a thoughtful new hire who reads the help text, asks the system what it can do, and uses the credentials already sitting on the laptop — no new servers, no new secrets, no new schemas, no new tax on every token of context. They scale down to nothing when unused and up to everything when needed. They are, in the deepest sense, &lt;em&gt;aligned with the grain of the system they run on&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;MCP, in turn, becomes what it should always have been: not the default front door for every integration, but the specialist tool for the cases where text and shell are genuinely insufficient — when you need to render a slide, drive a design canvas, hold an OAuth session, pool a database connection, stream binary assets through a protocol the terminal was never meant to carry.&lt;/p&gt;
&lt;p&gt;The choice is no longer "which protocol is more elegant." It is the older, harder, more interesting question that good engineering has always asked: &lt;em&gt;what is the smallest, cheapest, safest thing that will actually do the job?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For most of what we ask language models to do every day, the answer was sitting in &lt;code&gt;/usr/local/bin&lt;/code&gt; the whole time.&lt;/p&gt;</content><category term="AI"/><category term="llm"/><category term="mcp"/><category term="skills"/><category term="claude"/><category term="tokens"/><category term="security"/></entry><entry><title>MCP vs Skills: when to use which in professional LLM-assisted coding</title><link href="https://blog.r6i.it/mcp-vs-skills-when-to-use-which-in-professional-llm-assisted-coding.html" rel="alternate"/><published>2026-05-10T22:48:00+02:00</published><updated>2026-05-10T22:48:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-05-10:/mcp-vs-skills-when-to-use-which-in-professional-llm-assisted-coding.html</id><summary type="html">&lt;p&gt;Anyone who has spent years doing systems integration immediately understands the appeal of MCP. The dream in this field has always been the same: a clean contract between systems, where every method is described, every parameter is typed, every response is predictable. No more 80-page PDFs read half-heartedly, no more …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Anyone who has spent years doing systems integration immediately understands the appeal of MCP. The dream in this field has always been the same: a clean contract between systems, where every method is described, every parameter is typed, every response is predictable. No more 80-page PDFs read half-heartedly, no more reverse engineering undocumented APIs, no more "ask Dave, he knows how it works." Just a schema, and everything else follows.&lt;/p&gt;
&lt;p&gt;MCP delivers exactly that dream — but for language models. An MCP server exposes its tools with human-readable descriptions, structured parameters, defined types. The model reads the schema and &lt;em&gt;already knows what it can do and how to do it&lt;/em&gt;. For anyone coming from systems integration, it's almost moving: it feels like someone finally applied the principles of good API design to the problem of making LLMs collaborate with the outside world.&lt;/p&gt;
&lt;p&gt;The appeal is undeniable. And justified — MCP is genuinely a good idea, well executed.&lt;/p&gt;
&lt;p&gt;But it's not a silver bullet. Like almost every elegant solution in software architecture, it comes with trade-offs that only become visible when you start using it in production, on real problems, with real constraints of cost and latency.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The problem nobody tells you about&lt;/h2&gt;
&lt;p&gt;When a team starts using LLMs seriously for coding, they inevitably reach a fork in the road: how do you make the model interact with the outside world? How do you give it access to the right tools without turning every request into an exercise in token waste?&lt;/p&gt;
&lt;p&gt;Two dominant approaches exist: &lt;strong&gt;MCP servers&lt;/strong&gt; (Model Context Protocol) and &lt;strong&gt;CLI-based skills&lt;/strong&gt;. They are not equivalent. Choosing the wrong one has a real cost — in latency, inference costs, and maintenance complexity.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What they are, briefly&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; is a standardized protocol that allows the model to interact with external services through a structured interface. The MCP server exposes tools, resources, and prompts that the model can invoke natively, with authentication and state managed server-side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLI-based skills&lt;/strong&gt; are a more hands-on approach: you teach the model to use command-line tools by documenting how to invoke them, which flags they accept, what they return. The model learns to progressively "discover" the tool's capabilities, much like a developer consulting a man page for the first time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;When MCP is the right choice&lt;/h2&gt;
&lt;p&gt;MCP shines in one specific case: when you need to integrate &lt;strong&gt;complex external systems that don't naturally speak the language of text&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think Canva. Or Figma. Or a BI system that generates interactive charts, a DAW for audio production, a 3D rendering platform. These systems have rich APIs, complex state management, OAuth authentication, webhooks, binary assets. They are not designed to be consumed or produced as text — they are designed to be &lt;em&gt;used&lt;/em&gt;, with clicks, drags, graphical parameters, visual previews.&lt;/p&gt;
&lt;p&gt;In these cases, an MCP server does excellent work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Abstracts complexity&lt;/strong&gt;: the model doesn't need to know how Canva's API works internally. It only knows that a &lt;code&gt;create_presentation&lt;/code&gt; tool exists with certain parameters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manages state&lt;/strong&gt;: authentication, session tokens, user context — everything lives in the MCP server, not in the prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is type-safe&lt;/strong&gt;: MCP tools have defined schemas. The model receives structured feedback, not raw text to parse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is auditable&lt;/strong&gt;: every tool call is loggable, traceable, revocable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example: you want the model to automatically generate documentation slides from your code. With MCP you can integrate Canva or Google Slides directly, with access to company templates, brand fonts, layout systems — things that via CLI would be impossible or would require a massive wrapper.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;When CLI Skills are superior&lt;/h2&gt;
&lt;p&gt;For &lt;strong&gt;everyday professional coding&lt;/strong&gt;, CLI-based skills beat MCP on one fundamental point: &lt;strong&gt;token usage&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Consider what happens when you use an MCP server: at context initialization, the model must receive the full list of available tools, with their schemas, descriptions, and parameters. If the server exposes 40 tools (perfectly normal for an enterprise integration), you're burning hundreds or thousands of tokens &lt;em&gt;before making a single useful request&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;CLI skills work differently. They work the way an experienced developer works: in a &lt;strong&gt;progressive and contextual&lt;/strong&gt; manner.&lt;/p&gt;
&lt;p&gt;The model learns that a tool exists (say &lt;code&gt;gh&lt;/code&gt; for GitHub CLI, or &lt;code&gt;kubectl&lt;/code&gt;, or &lt;code&gt;terraform&lt;/code&gt;). When it needs to do something, it can invoke &lt;code&gt;gh --help&lt;/code&gt; or &lt;code&gt;gh issue --help&lt;/code&gt; to discover exactly the flags it needs, &lt;em&gt;at the moment it needs them&lt;/em&gt;. It doesn't hold everything in its head from the start — it discovers progressively.&lt;/p&gt;
&lt;p&gt;This leads to concrete advantages:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token efficiency&lt;/strong&gt;: context grows only where needed. If the task is "create a PR with this diff", the model uses 3-4 commands. It doesn't load documentation for all 200 &lt;code&gt;gh&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unix composability&lt;/strong&gt;: CLIs compose. &lt;code&gt;git log --oneline | grep "fix" | head -20&lt;/code&gt; is a powerful operation combining three tools. With MCP, the same result would require separate calls or a dedicated tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Natural debugging&lt;/strong&gt;: when a command fails, the error output is text. The model reads it, understands, corrects. With MCP, errors pass through an extra layer of abstraction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero infrastructure setup&lt;/strong&gt;: a CLI skill is a text file documenting how to use an already-existing tool. An MCP server is a service to deploy, maintain, and monitor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The progressive discovery pattern&lt;/h2&gt;
&lt;p&gt;This is the most underrated aspect of CLI skills, and it's worth dwelling on.&lt;/p&gt;
&lt;p&gt;A developer who doesn't know a tool doesn't read the 300-page manual before starting. They type the command, read the help, try, fail, try again. The feedback loop is fast and contextual.&lt;/p&gt;
&lt;p&gt;LLMs can do the same thing — and they should.&lt;/p&gt;
&lt;p&gt;A well-written skill is not an encyclopedic dump of documentation. It's a starting point:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform skill&lt;/span&gt;
&lt;span class="k"&gt;Use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n n-Quoted"&gt;`terraform`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;manage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;infrastructure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;point&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n n-Quoted"&gt;`terraform --help`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;discover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subcommands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Common&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subcommand&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n n-Quoted"&gt;`terraform &amp;lt;subcommand&amp;gt; --help`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The model will start from this base and &lt;em&gt;build&lt;/em&gt; understanding of the tool during execution, exactly like a junior developer in their first sprint with a new technology.&lt;/p&gt;
&lt;p&gt;This directly contrasts with the MCP approach, where all tools must be declared upfront in the server schema — the model knows everything from the start, but pays for that "everything" in tokens on every call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;A practical decision matrix&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;th&gt;CLI Skill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-textual external system (graphics, audio, BI)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth authentication / complex sessions&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools already available as CLI&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limited token budget&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small team, zero infrastructure to manage&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured audit trail required&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unix pipe composability&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise SaaS system integration&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;The hybrid case: you don't have to choose&lt;/h2&gt;
&lt;p&gt;In a mature professional coding workflow, the right answer is often hybrid:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt; for integrations with external systems that genuinely require it (the design system in Figma, notifications in Slack, deploys on platforms with complex APIs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI skills&lt;/strong&gt; for the entire development toolchain: git, Docker, kubectl, terraform, test runners, linters, package managers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key point is not to use MCP out of laziness (because "it's more modern") or CLI skills out of frugality (because "it's simpler") — but to choose based on the nature of the system being integrated and the real token cost of each interaction.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;MCP and CLI skills are not in competition — they solve different problems.&lt;/p&gt;
&lt;p&gt;MCP is powerful when you need to bring the model into the territory of systems that don't speak text: graphical systems, creative platforms, SaaS services with complex state. It's the only sensible way to do things like "generate a Canva presentation from my README."&lt;/p&gt;
&lt;p&gt;CLI skills are gold in everyday professional coding, where tools are already excellent, already composable, already battle-tested by decades of Unix philosophy — and where every unused token is money saved and latency reduced.&lt;/p&gt;
&lt;p&gt;Progressive CLI discovery, moreover, is not a limitation: it's a feature. It teaches the model to reason like a good developer — starting from the bare minimum and building understanding only where needed.&lt;/p&gt;</content><category term="blog"/><category term="AI"/><category term="LLM"/><category term="MCP"/><category term="Python"/></entry><entry><title>Running Gemma 4 31B on an Apple Silicon Mac with Ollama</title><link href="https://blog.r6i.it/running-gemma-4-31b-on-an-apple-silicon-mac-with-ollama.html" rel="alternate"/><published>2026-04-26T22:48:00+02:00</published><updated>2026-04-26T22:48:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-04-26:/running-gemma-4-31b-on-an-apple-silicon-mac-with-ollama.html</id><summary type="html">&lt;h2&gt;A practical configuration for a 32 GB M5 Mac that still needs to remain usable&lt;/h2&gt;
&lt;p&gt;Running large language models locally has become surprisingly practical on Apple Silicon. With a modern Mac, Ollama, and a carefully quantized GGUF model, it is possible to run models that only a short time ago …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;A practical configuration for a 32 GB M5 Mac that still needs to remain usable&lt;/h2&gt;
&lt;p&gt;Running large language models locally has become surprisingly practical on Apple Silicon. With a modern Mac, Ollama, and a carefully quantized GGUF model, it is possible to run models that only a short time ago would have felt out of reach for a personal machine.&lt;/p&gt;
&lt;p&gt;This post collects the practical findings from configuring &lt;strong&gt;Gemma 4 31B&lt;/strong&gt; on a &lt;strong&gt;32 GB Apple Silicon Mac with an M5 processor&lt;/strong&gt;, using Ollama and a highly compressed GGUF quantization.&lt;/p&gt;
&lt;p&gt;The goal is not to squeeze every last token per second out of the machine. The goal is more realistic:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Run a capable 31B local model while keeping the Mac usable for normal work: browser, IDE, terminal, notes, chat apps, and light development tools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distinction matters. A configuration that works for a benchmark is not necessarily a configuration you want to live with all day.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The model&lt;/h2&gt;
&lt;p&gt;The model used in this setup is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gemma-4-31B-it-UD-IQ3_XXS.gguf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is an aggressively quantized GGUF build of Gemma 4 31B. The &lt;code&gt;IQ3_XXS&lt;/code&gt; quantization makes the model small enough to fit into machines that would otherwise be unable to run a 31B model at all.&lt;/p&gt;
&lt;p&gt;The trade-off is obvious: this is not the highest quality quantization, but it gives access to a much larger model class on consumer hardware.&lt;/p&gt;
&lt;p&gt;The Ollama &lt;code&gt;Modelfile&lt;/code&gt; starts from the local GGUF file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2&gt;Understanding the main Ollama parameters&lt;/h2&gt;
&lt;p&gt;Before tuning the configuration, it helps to understand what the key parameters actually control.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;num_ctx&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;num_ctx&lt;/code&gt; controls the maximum context window used by the model.&lt;/p&gt;
&lt;p&gt;A larger context means the model can keep more conversation, documents, code, or instructions in memory. But it also increases memory usage, especially through the KV cache.&lt;/p&gt;
&lt;p&gt;For this model, useful values are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;6144   conservative
8192   balanced
12288  aggressive
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For daily use on a 32 GB Mac, &lt;code&gt;8192&lt;/code&gt; is a good target.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;num_batch&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;num_batch&lt;/code&gt; affects how many tokens are processed together during prompt ingestion.&lt;/p&gt;
&lt;p&gt;It mostly impacts the speed at which the model reads the input prompt, not necessarily the speed at which it generates the answer token by token.&lt;/p&gt;
&lt;p&gt;Higher values can improve responsiveness with longer prompts, but they also increase temporary memory pressure.&lt;/p&gt;
&lt;p&gt;Good values for this setup are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;32   conservative
64   balanced
96   aggressive
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For a daily driver configuration, &lt;code&gt;64&lt;/code&gt; is a reasonable compromise. If the Mac becomes sluggish or the runner crashes, this is one of the first values to reduce.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;num_gpu&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In Ollama, &lt;code&gt;num_gpu&lt;/code&gt; does not mean “number of GPUs”. It means how many model layers are offloaded to the GPU.&lt;/p&gt;
&lt;p&gt;For Gemma 4 31B, the theoretical maximum is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;num_gpu 60
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;because the model has 60 layers.&lt;/p&gt;
&lt;p&gt;However, full offload is not always the best practical choice. On a machine that must remain usable for other work, leaving some margin is often better than maximizing GPU offload.&lt;/p&gt;
&lt;p&gt;For a 32 GB M5 Mac, a good range is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;50   conservative
55   balanced
60   aggressive / full offload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The recommended daily value is &lt;code&gt;55&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The recommended daily configuration&lt;/h2&gt;
&lt;p&gt;This is the configuration I would use as a balanced daily driver.&lt;/p&gt;
&lt;p&gt;It gives a useful context window, keeps prompt processing reasonably fast, and avoids pushing the system too close to the edge.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf

PARAMETER num_ctx 8192
PARAMETER num_batch 64
PARAMETER num_gpu 55

PARAMETER temperature 1.0
PARAMETER top_p 0.95
PARAMETER top_k 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create or replace the model with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/ollama-models/gemma4-31b-iq3

cat&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;Modelfile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;&lt;/span&gt;
&lt;span class="s"&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf&lt;/span&gt;

&lt;span class="s"&gt;PARAMETER num_ctx 8192&lt;/span&gt;
&lt;span class="s"&gt;PARAMETER num_batch 64&lt;/span&gt;
&lt;span class="s"&gt;PARAMETER num_gpu 55&lt;/span&gt;

&lt;span class="s"&gt;PARAMETER temperature 1.0&lt;/span&gt;
&lt;span class="s"&gt;PARAMETER top_p 0.95&lt;/span&gt;
&lt;span class="s"&gt;PARAMETER top_k 64&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;

ollama&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;gemma4-31b-iq3xxs-32gb
ollama&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;gemma4-31b-iq3xxs-32gb
ollama&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;gemma4-31b-iq3xxs-32gb&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;./Modelfile
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then test it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ollama&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;gemma4-31b-iq3xxs-32gb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Write a Python hello world.&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2&gt;Environment variables&lt;/h2&gt;
&lt;p&gt;For this setup, Flash Attention and quantized KV cache are useful.&lt;/p&gt;
&lt;p&gt;If using the Ollama macOS app, set the variables with &lt;code&gt;launchctl&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;q8_0&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;8192&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KEEP_ALIVE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5m&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then quit and reopen Ollama from the menu bar.&lt;/p&gt;
&lt;p&gt;If running Ollama manually from the terminal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_FLASH_ATTENTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_KV_CACHE_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;q8_0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_CONTEXT_LENGTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_KEEP_ALIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
ollama&lt;span class="w"&gt; &lt;/span&gt;serve
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2&gt;What &lt;code&gt;OLLAMA_KV_CACHE_TYPE=q8_0&lt;/code&gt; does&lt;/h2&gt;
&lt;p&gt;The KV cache stores the internal attention state for tokens that have already been processed. It grows with the active context length.&lt;/p&gt;
&lt;p&gt;Using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_KV_CACHE_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;q8_0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;asks Ollama to store the KV cache in an 8-bit quantized format.&lt;/p&gt;
&lt;p&gt;The practical effect is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;lower memory usage
better support for longer context windows
usually minimal quality loss compared to f16 KV cache
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It is especially useful when running large models with larger context windows.&lt;/p&gt;
&lt;p&gt;However, it does not significantly reduce the memory used by the model weights themselves. It mainly helps with memory used by context.&lt;/p&gt;
&lt;p&gt;For this reason, &lt;code&gt;q8_0&lt;/code&gt; is useful, but it is not a magic fix for every memory issue.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Flash Attention&lt;/h2&gt;
&lt;p&gt;Flash Attention should be enabled when using quantized KV cache:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_FLASH_ATTENTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It helps reduce memory usage and can improve efficiency with larger contexts.&lt;/p&gt;
&lt;p&gt;For this model, I would keep it enabled.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Keeping the Mac usable&lt;/h2&gt;
&lt;p&gt;Apple Silicon uses unified memory. The CPU and GPU share the same memory pool.&lt;/p&gt;
&lt;p&gt;That means a local LLM can directly compete with everything else on the Mac:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;browser tabs
IDE
Docker
database containers
video calls
Slack or Teams
file indexing
other development tools
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is why the best configuration is not necessarily the most aggressive one.&lt;/p&gt;
&lt;p&gt;The aim is to leave enough headroom that the system does not constantly swap or become unresponsive.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Wired memory limit&lt;/h2&gt;
&lt;p&gt;Some users increase the Metal wired memory limit with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On a 32 GB Mac, I would use one of these values:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;20000  conservative workstation mode
22000  balanced daily mode
24000  LLM-priority mode
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For daily work, I would start with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I would avoid pushing this too close to total system memory. Values like 28000 or 30000 may help a benchmark but can make the machine unpleasant to use for normal work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Three practical profiles&lt;/h2&gt;
&lt;h3&gt;1. Workstation profile&lt;/h3&gt;
&lt;p&gt;Use this when you also need Docker, an IDE, many browser tabs, or other heavy tools.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf

PARAMETER num_ctx 6144
PARAMETER num_batch 32
PARAMETER num_gpu 50

PARAMETER temperature 1.0
PARAMETER top_p 0.95
PARAMETER top_k 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Recommended environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;20000&lt;/span&gt;

launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;q8_0&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;6144&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KEEP_ALIVE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2m&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the safest profile for a Mac that must remain responsive.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. Balanced daily profile&lt;/h3&gt;
&lt;p&gt;This is the recommended default.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf

PARAMETER num_ctx 8192
PARAMETER num_batch 64
PARAMETER num_gpu 55

PARAMETER temperature 1.0
PARAMETER top_p 0.95
PARAMETER top_k 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Recommended environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;

launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;q8_0&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;8192&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KEEP_ALIVE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5m&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the profile I would use most of the time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. LLM-priority profile&lt;/h3&gt;
&lt;p&gt;Use this when the Mac is mostly dedicated to local inference.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf

PARAMETER num_ctx 12288
PARAMETER num_batch 96
PARAMETER num_gpu 60

PARAMETER temperature 1.0
PARAMETER top_p 0.95
PARAMETER top_k 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Recommended environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;24000&lt;/span&gt;

launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;q8_0&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;12288&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KEEP_ALIVE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;10m&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is more aggressive. It may be useful for focused LLM sessions, but I would not use it while doing normal development work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;How to verify what is happening&lt;/h2&gt;
&lt;p&gt;After running the model, check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ollama&lt;span class="w"&gt; &lt;/span&gt;ps
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This shows whether the model is loaded and how it is using the available processors.&lt;/p&gt;
&lt;p&gt;To inspect logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-iE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;offload|layers|gpu|metal|memory&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.ollama/logs/server.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Look for messages indicating how many layers were offloaded to the GPU.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;offloaded 55/60 layers to GPU
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That confirms that &lt;code&gt;num_gpu 55&lt;/code&gt; is actually being applied.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Troubleshooting instability&lt;/h2&gt;
&lt;p&gt;If Ollama crashes or the Mac becomes sluggish, reduce parameters in this order.&lt;/p&gt;
&lt;p&gt;First reduce &lt;code&gt;num_batch&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;64 -&amp;gt; 32
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then reduce &lt;code&gt;num_ctx&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;8192 -&amp;gt; 6144
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then reduce &lt;code&gt;num_gpu&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;55 -&amp;gt; 50
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally reduce the wired memory limit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;22000 -&amp;gt; 20000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A stable machine is more useful than a theoretical maximum configuration that crashes during real work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;About Metal crashes&lt;/h2&gt;
&lt;p&gt;During experimentation, one possible failure mode is a Metal backend crash, with logs similar to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ggml-metal-device.m:608: GGML_ASSERT([rsets-&amp;gt;data count] == 0) failed
panic during panic
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When this happens, it is often better to reset the setup rather than keep pushing the same configuration.&lt;/p&gt;
&lt;p&gt;A practical recovery sequence is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;killall&lt;span class="w"&gt; &lt;/span&gt;Ollama
killall&lt;span class="w"&gt; &lt;/span&gt;ollama

launchctl&lt;span class="w"&gt; &lt;/span&gt;unsetenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION
launchctl&lt;span class="w"&gt; &lt;/span&gt;unsetenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE
launchctl&lt;span class="w"&gt; &lt;/span&gt;unsetenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH

sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;20000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then reboot the Mac and restart from a conservative profile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Final recommendation&lt;/h2&gt;
&lt;p&gt;For a 32 GB M5 Mac that should remain useful as a normal workstation, I would use this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sysctl&lt;span class="w"&gt; &lt;/span&gt;iogpu.wired_limit_mb&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;

launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_FLASH_ATTENTION&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KV_CACHE_TYPE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;q8_0&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_CONTEXT_LENGTH&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;8192&amp;quot;&lt;/span&gt;
launchctl&lt;span class="w"&gt; &lt;/span&gt;setenv&lt;span class="w"&gt; &lt;/span&gt;OLLAMA_KEEP_ALIVE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5m&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And this &lt;code&gt;Modelfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM ./gemma-4-31B-it-UD-IQ3_XXS.gguf

PARAMETER num_ctx 8192
PARAMETER num_batch 64
PARAMETER num_gpu 55

PARAMETER temperature 1.0
PARAMETER top_p 0.95
PARAMETER top_k 64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is not the most extreme configuration. It is the one I would actually want to use.&lt;/p&gt;
&lt;p&gt;It gives enough context for serious work, enough GPU offload for acceptable performance, and enough memory headroom to keep the Mac usable while doing other things.&lt;/p&gt;
&lt;p&gt;That is usually the sweet spot for local LLMs: not maximum throughput, but sustainable performance.&lt;/p&gt;</content><category term="blog"/><category term="AI"/><category term="LLM"/><category term="MCP"/><category term="Python"/></entry><entry><title>Giving voice to Italian energy markets with artificial intelligence</title><link href="https://blog.r6i.it/giving-voice-to-italian-energy-markets-with-artificial-intelligence.html" rel="alternate"/><published>2026-04-12T22:48:00+02:00</published><updated>2026-04-12T22:48:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-04-12:/giving-voice-to-italian-energy-markets-with-artificial-intelligence.html</id><summary type="html">&lt;h2&gt;How an MCP server turns electricity exchange data into natural conversations&lt;/h2&gt;
&lt;p&gt;The Italian electricity market produces an enormous amount of data every day: hourly prices by zone, traded volumes, liquidity, the PUN (Prezzo Unico Nazionale — the national single price). Essential data for traders, energy analysts, industry journalists, and companies operating …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;How an MCP server turns electricity exchange data into natural conversations&lt;/h2&gt;
&lt;p&gt;The Italian electricity market produces an enormous amount of data every day: hourly prices by zone, traded volumes, liquidity, the PUN (Prezzo Unico Nazionale — the national single price). Essential data for traders, energy analysts, industry journalists, and companies operating in the energy sector. Yet accessing this data still means navigating web portals, downloading CSVs, and writing ad-hoc scripts. A fragmented and slow workflow.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;mercati-energetici-mcp&lt;/strong&gt; I set out to change the approach: making GME (Gestore dei Mercati Energetici — the Italian Energy Markets Operator) data directly accessible from within the AI assistants we already use every day — Claude, Copilot, and any other tool compatible with the MCP protocol.&lt;/p&gt;
&lt;h2&gt;What is MCP and why it changes the game&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;Model Context Protocol&lt;/strong&gt; is an open standard that allows language models to interact with external services in a structured way. Think of it as a bridge: on one side there's the AI that knows how to reason, on the other there's real-world data. MCP connects them.&lt;/p&gt;
&lt;p&gt;Before MCP, getting an LLM to access market data required building custom integrations, prompt engineering pipelines, or manually copying and pasting data into the conversation context. With MCP, you simply declare &lt;strong&gt;tools&lt;/strong&gt; — functions the AI can invoke autonomously whenever it needs them.&lt;/p&gt;
&lt;p&gt;The difference is substantial: you no longer have to retrieve the data yourself and then ask the AI to analyze it. The AI itself, during the conversation, decides when and which data it needs and retrieves it on its own.&lt;/p&gt;
&lt;h2&gt;The implementation: Python, FastMCP, and simplicity&lt;/h2&gt;
&lt;p&gt;The project is intentionally minimal. A single Python file, about 150 lines of code, exposing 5 tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;get_prices&lt;/strong&gt; — hourly energy prices by zone and day&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;get_volumes&lt;/strong&gt; — bought and sold volumes on the market&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;get_liquidity&lt;/strong&gt; — hourly market liquidity percentage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;daily_pun&lt;/strong&gt; — daily national average price&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;get_zones&lt;/strong&gt; — list of available market zones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href="https://github.com/sammyrulez/mercati-energetici"&gt;mercati-energetici&lt;/a&gt; library handles the interaction with the official GME APIs. &lt;a href="https://github.com/jlowin/fastmcp"&gt;FastMCP&lt;/a&gt; manages the entire MCP protocol, turning simple Python functions decorated with &lt;code&gt;@server.tool()&lt;/code&gt; into endpoints that any MCP client can discover and invoke.&lt;/p&gt;
&lt;p&gt;The code for a tool is disarmingly simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;PUN&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Get electricity prices in €/MWh for a specific day and zone.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;init_MGP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mgp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mgp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;zone&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;prices&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;unit&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;€/MWh&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;zone&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No boilerplate, no complex configuration. The docstring automatically becomes the description the AI reads to understand when and how to use the tool. The type hints on parameters become the JSON schema that the protocol uses for validation.&lt;/p&gt;
&lt;h2&gt;Integration with Claude and Copilot&lt;/h2&gt;
&lt;h3&gt;Claude Desktop and Claude Code&lt;/h3&gt;
&lt;p&gt;Once the server is configured in Claude's config, electricity market data becomes a natural part of the conversation. You can ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"What was the price of energy in Sicily yesterday?"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude doesn't search the internet, doesn't hallucinate a number: it invokes &lt;code&gt;get_prices&lt;/code&gt; with the correct parameters and returns the actual data from GME. You can go further:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Compare today's prices between Northern and Southern Italy and tell me if there's a significant gradient"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The AI calls the tool twice, once per zone, and then reasons on the real data to produce an analysis.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Show me the PUN trend over the last week and identify any anomalies"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here Claude chains multiple calls to &lt;code&gt;daily_pun&lt;/code&gt; for different days, builds a time series, and applies its analytical reasoning on concrete data.&lt;/p&gt;
&lt;h3&gt;GitHub Copilot&lt;/h3&gt;
&lt;p&gt;With MCP support expanding across the Copilot ecosystem as well, the same server can power energy analyses directly within the IDE. A data analyst writing Python code for a report can have market data at prompt's reach, without leaving VS Code.&lt;/p&gt;
&lt;h3&gt;Any MCP client&lt;/h3&gt;
&lt;p&gt;The beauty of an open standard is that you're not locked into a vendor. Any tool that speaks MCP — present or future — can connect to this server and access Italian electricity market data.&lt;/p&gt;
&lt;h2&gt;Concrete use cases&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;For the energy trader&lt;/strong&gt;: quick analysis of zonal prices, cross-zone comparison, pattern identification in traded volumes. All in natural language, without having to open spreadsheets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the industry analyst&lt;/strong&gt;: automatic generation of daily or weekly reports on the electricity market, with AI-produced commentary and interpretations based on real data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the journalist&lt;/strong&gt;: immediate access to market data for articles and in-depth pieces, with the ability to ask the AI for explanations and context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the developer&lt;/strong&gt;: a concrete example of how to build an MCP server for a vertical domain, reusable as a template for other markets or data sources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the energy company&lt;/strong&gt;: integration of exchange data into internal workflows, from customer service answering pricing questions to the procurement office monitoring the market.&lt;/p&gt;
&lt;h2&gt;The value of the MCP approach&lt;/h2&gt;
&lt;p&gt;The real innovation isn't in the code — it's in the paradigm. We're moving from a model where the user is the intermediary between data and AI, to one where the AI accesses data directly and uses it within the conversation context.&lt;/p&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No more copy-paste&lt;/strong&gt;: data flows directly into the AI's reasoning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Always up-to-date data&lt;/strong&gt;: every request queries the GME APIs in real time&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual analysis&lt;/strong&gt;: the AI can combine data from multiple calls to produce insights that would otherwise require manual work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;: anyone can query the electricity market in natural language, without specific technical skills&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;The project is open source under the MIT license. To try it you need:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Python 3.12+ and &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt; as a package manager&lt;/li&gt;
&lt;li&gt;A GME account with API access (request it at &lt;a href="https://www.mercatoelettrico.org/"&gt;mercatoelettrico.org&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An MCP client — Claude Desktop, Claude Code, or any other compatible one&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/sammyrulez/mercati-energetici-mcp
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mercati-energetici-mcp
uv&lt;span class="w"&gt; &lt;/span&gt;sync
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Configure your credentials and you're up and running. Less than 5 minutes from installation to your first natural language query about energy prices.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Italian electricity market is a data-rich domain but historically not very accessible. With an MCP server of just a few lines of code, this data becomes an integral part of AI conversations — transforming a fragmented workflow into a fluid and natural dialogue.&lt;/p&gt;
&lt;p&gt;It's a small project, but it represents a powerful pattern: any structured data source can become an MCP tool, and therefore become accessible through natural language. Energy markets are just the beginning.&lt;/p&gt;</content><category term="blog"/><category term="AI"/><category term="LLM"/><category term="MCP"/><category term="Python"/></entry><entry><title>Stop Putting Secrets in Your Repo. Seriously.</title><link href="https://blog.r6i.it/stop-putting-secrets-in-your-repo-seriously.html" rel="alternate"/><published>2026-03-27T22:48:00+01:00</published><updated>2026-03-27T22:48:00+01:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2026-03-27:/stop-putting-secrets-in-your-repo-seriously.html</id><summary type="html">&lt;p&gt;OK. We've all been there. You're setting up a new microservice, you need a database connection string, a Keycloak client secret, an OCIR auth token — and before you know it, you're copy-pasting credentials into a &lt;code&gt;.tfvars&lt;/code&gt; file and telling yourself &lt;em&gt;"I'll fix this later"&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Later never comes. Until it does …&lt;/p&gt;</summary><content type="html">&lt;p&gt;OK. We've all been there. You're setting up a new microservice, you need a database connection string, a Keycloak client secret, an OCIR auth token — and before you know it, you're copy-pasting credentials into a &lt;code&gt;.tfvars&lt;/code&gt; file and telling yourself &lt;em&gt;"I'll fix this later"&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Later never comes. Until it does, and it's painful.&lt;/p&gt;
&lt;p&gt;This post walks through how we wired up &lt;strong&gt;OCI Vault&lt;/strong&gt; with &lt;strong&gt;Terraform&lt;/strong&gt; to manage secrets properly — and why the side effects are even better than the main benefit.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The Problem with &lt;code&gt;TF_VAR_&lt;/code&gt; and Plain tfvars&lt;/h2&gt;
&lt;p&gt;The classic approach for passing secrets to Terraform is either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;export TF_VAR_my_secret="..."&lt;/code&gt; before every apply&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;secrets.tfvars&lt;/code&gt; file conveniently added to &lt;code&gt;.gitignore&lt;/code&gt; (and inevitably forgotten on a new machine)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both approaches work until they don't:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new team member clones the repo and has no idea what secrets to set&lt;/li&gt;
&lt;li&gt;CI/CD needs the secret injected as a pipeline variable — now you're duplicating it&lt;/li&gt;
&lt;li&gt;Someone accidentally commits the file. It happens.&lt;/li&gt;
&lt;li&gt;You rotate a credential and have to remember every place it's used&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The real issue is that secrets live &lt;strong&gt;outside your infrastructure definition&lt;/strong&gt;. They're implicit, undocumented, and fragile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Enter OCI Vault&lt;/h2&gt;
&lt;p&gt;OCI Vault is Oracle Cloud's managed secret store. Think of it as a safe deposit box that Terraform can open at plan/apply time. You store the secret once, reference it by OCID, and Terraform fetches the value directly — no environment variables, no hidden files.&lt;/p&gt;
&lt;h3&gt;Creating a secret&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;oci&lt;span class="w"&gt; &lt;/span&gt;vault&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;create-base64&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--compartment-id&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$COMPARTMENT_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--vault-id&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VAULT_OCID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--key-id&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KEY_OCID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--secret-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my-service-db-password&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--secret-content-content&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;supersecret&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;base64&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's it. The secret now has an OCID you can reference anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Wiring It Into Terraform&lt;/h2&gt;
&lt;p&gt;The integration is clean. Declare a &lt;code&gt;data&lt;/code&gt; source, decode the content, use it where you need it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;&amp;quot;oci_secrets_secretbundle&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;&amp;quot;db_password&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;secret_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;var.db_password_secret_ocid&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;&amp;quot;microservice&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;app_secrets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;Oracle__ConnectionString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;base64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;data.oci_secrets_secretbundle.db_password.secret_bundle_content[0].content&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&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;Your &lt;code&gt;common.tfvars&lt;/code&gt; now contains only an OCID — a pointer, not a value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;db_password_secret_ocid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ocid1.vaultsecret.oc1.eu-miami-1.amaaaaaaxxx&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;An OCID is not a secret. You can commit it. You can share it. You can put it in a ticket. The actual value stays in the Vault, protected by IAM policies and audit logs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The Real Win: A Repo You Can Actually Share&lt;/h2&gt;
&lt;p&gt;Here's the part that doesn't get enough attention.&lt;/p&gt;
&lt;p&gt;When your secrets live in a Vault and your repo contains only OCIDs and config, your entire infrastructure definition becomes &lt;strong&gt;fully committable&lt;/strong&gt;. No &lt;code&gt;.gitignore&lt;/code&gt; exceptions. No "ask a colleague for the secrets file". No onboarding friction.&lt;/p&gt;
&lt;p&gt;A developer cloning the repo for the first time gets everything they need to understand the infrastructure — and with the right IAM permissions, they can run &lt;code&gt;terraform plan&lt;/code&gt; immediately.&lt;/p&gt;
&lt;p&gt;This changes the dynamic of your infrastructure code from &lt;em&gt;"mostly version controlled"&lt;/em&gt; to &lt;em&gt;"fully version controlled"&lt;/em&gt;. And that matters more than it sounds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pull requests for infrastructure changes are complete&lt;/strong&gt; — reviewers see the full picture, not just the non-sensitive half&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git history is trustworthy&lt;/strong&gt; — a &lt;code&gt;git log&lt;/code&gt; on &lt;code&gt;main.tf&lt;/code&gt; tells the real story of what changed and when&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollbacks are safe&lt;/strong&gt; — checking out a previous commit gives you a working state, not a broken one missing secret references&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Portability Across Environments and Teams&lt;/h2&gt;
&lt;p&gt;With this setup, spinning up a new environment is a two-step operation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create the secrets in the Vault for the new environment&lt;/li&gt;
&lt;li&gt;Add the OCIDs to the new &lt;code&gt;envs/staging.tfvars&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No secret synchronization. No "which version of the connection string does staging use?". The Vault is the source of truth, and Terraform reads from it at apply time.&lt;/p&gt;
&lt;p&gt;The same applies when onboarding a new microservice. Copy the &lt;code&gt;infra_template/&lt;/code&gt; directory, fill in the placeholders, point to the right Vault OCIDs — done. The template is self-documenting because there's nothing hidden.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Secret Rotation Without Drama&lt;/h2&gt;
&lt;p&gt;Rotating a secret used to mean: update the value in five places, hope you didn't miss one, redeploy everything, debug the one service that broke because you forgot to update its CI variable.&lt;/p&gt;
&lt;p&gt;With Vault:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;oci&lt;span class="w"&gt; &lt;/span&gt;vault&lt;span class="w"&gt; &lt;/span&gt;secret&lt;span class="w"&gt; &lt;/span&gt;update-base64&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--secret-id&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SECRET_OCID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--secret-content-content&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;new-value&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;base64&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then &lt;code&gt;terraform apply&lt;/code&gt;. Terraform fetches the new value, updates the Kubernetes secret, and the pod picks it up. One place, one command, full audit trail.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;A Note on the Audit Trail&lt;/h2&gt;
&lt;p&gt;Every access to a Vault secret is logged in OCI Audit. You know who read what and when. This is not just a compliance checkbox — it's genuinely useful when you're debugging a production incident and need to know whether a secret was accessed or rotated in the last 24 hours.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets in &lt;code&gt;.tfvars&lt;/code&gt;, gitignored&lt;/td&gt;
&lt;td&gt;Only OCIDs in tfvars, fully committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export TF_VAR_secret=...&lt;/code&gt; before every apply&lt;/td&gt;
&lt;td&gt;No environment variables needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual secret rotation in multiple places&lt;/td&gt;
&lt;td&gt;One update in Vault, one &lt;code&gt;terraform apply&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding requires sharing secret files&lt;/td&gt;
&lt;td&gt;Clone repo, get IAM access, run plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial git history&lt;/td&gt;
&lt;td&gt;Complete, trustworthy git history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Vault setup takes an afternoon. The benefits compound forever. It's one of those infrastructure investments that quietly pays off every single day — even when nothing goes wrong.&lt;/p&gt;
&lt;p&gt;Especially when nothing goes wrong.&lt;/p&gt;</content><category term="blog"/><category term="devops"/><category term="terraform"/><category term="secops"/><category term="dataengineering"/></entry><entry><title>Forecasting Time Series from a COVID-19 Red Zone</title><link href="https://blog.r6i.it/forecasting-time-series-from-a-covid-19-red-zone.html" rel="alternate"/><published>2020-04-17T22:48:00+02:00</published><updated>2020-04-17T22:48:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2020-04-17:/forecasting-time-series-from-a-covid-19-red-zone.html</id><summary type="html">&lt;p&gt;Hello, dear citizen of the free world. I’m writing while I’m in a restricted lifestyle. On Monday night Italy’s prime minister decided that the entire country would be covered by restrictions that can be summarised as follows:&lt;/p&gt;
&lt;p&gt;“I stay at home”&lt;/p&gt;
&lt;p&gt;All travel was banned unless justified …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hello, dear citizen of the free world. I’m writing while I’m in a restricted lifestyle. On Monday night Italy’s prime minister decided that the entire country would be covered by restrictions that can be summarised as follows:&lt;/p&gt;
&lt;p&gt;“I stay at home”&lt;/p&gt;
&lt;p&gt;All travel was banned unless justified on professional or health grounds. You can go out to buy food and medications and nothing else. You can’t visit your parents if they are in a different neighborhood. I’m working remotely 100% of the time and trying to survive with two kids with a lot of spare time 😰.&lt;/p&gt;
&lt;p&gt;The situation is serious just because the health care system is at its maximum capacity and lives of the elderly and other people with serious chronic afflictions are at stake. That said everything is fine: we have food and the sun shines as always in Italy.&lt;/p&gt;
&lt;p&gt;So since I was already playing around with the Facebook Prophet library, I’m trying to put something together.&lt;/p&gt;
&lt;p&gt;The government has been so efficient, beyond any expectation, to set up a Github repo with all the data cand even to update them daily!&lt;/p&gt;
&lt;p&gt;I focus my effort on my province since there is less noise (regional and countrywide data could be misleading because the lockdown happened in different moments) and we are near the initial outbreak. And also I live here!&lt;/p&gt;
&lt;p&gt;Disclaimer: This is more an exercise in building a time series pipeline rather than find significant data on when exactly this situation will end. I’m no epidemiologist. There are specific models for that and I’m purposely using one from a social network.&lt;/p&gt;
&lt;p&gt;So the situation seems to flat out after 22 / 25 days.&lt;/p&gt;
&lt;p&gt;So I clean up the data with pandas, filtered just the rows about my province and shape them to suit Prophet requirements&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sigla_provincia&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;BS&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;bs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;data&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;bs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stato&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;codice_regione&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;denominazione_regione&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;codice_provincia&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sigla_provincia&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;long&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;denominazione_provincia&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;axis&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;bs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then I run 50 days forecast on this data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Prophet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;make_future_dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;forecast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*KV-99lnw_6pKx9JcKDnBUw.png"&gt;image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That is suspicious: we had more cases than in China. I have more confidence with the Italian data, but since the curve is monotonously increasing the prediction will just follow. In the hope to make some sense in this forecast, I added a regressor that made some sense: the number of days since the lockdown in Italy. Sadly is the same day that the data started to be recorded so it does not add much information. So I added two seasonalities to model the expected peak after 25 days after the first case and another after 14 days when new infections are less than the healed ones.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Prophet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_regressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;peak_reg&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_seasonality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;peak_period_start&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;fourier_order&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;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_seasonality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;peak_period_end&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;fourier_order&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;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;make_future_dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ds_ts&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;no_school&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2020&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;since_no_school&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ds_ts&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;no_school&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;axis&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;peak_reg&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;since_no_school&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peak_days&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;forecast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;miro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nl"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nl"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="n"&gt;webp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;uGUU8V4cwMeSz2IFWlr4yA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;probably&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overfitting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;few&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;so&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;So&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;infected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;predicted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exaggerated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;estimation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;But&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;estimated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;same&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;epidemiologist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;going&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enhance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;estimations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Worst&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;second&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;that&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;higher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ha&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reached&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;learned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;something&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Facebook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Prophet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Best&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lockdown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;office&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="blog"/><category term="ML"/></entry><entry><title>I will ride like a gentlemen</title><link href="https://blog.r6i.it/i-will-ride-like-a-gentlemen.html" rel="alternate"/><published>2017-04-17T22:48:00+02:00</published><updated>2017-04-17T22:48:00+02:00</updated><author><name>samreghenzi</name></author><id>tag:blog.r6i.it,2017-04-17:/i-will-ride-like-a-gentlemen.html</id><summary type="html">&lt;h1&gt;I will ride like a gentlemen&lt;/h1&gt;
&lt;p&gt;On September 24th I will ride in the Distinguished Gentlemen Ride&lt;/p&gt;
&lt;p&gt;&lt;img alt="This is me an my bike at a recent Ducati Scrambler event" src="https://cdn-images-1.medium.com/max/2000/1*_2xCEsRJxtbLU8uSnI9n0w.jpeg"&gt;&lt;em&gt;This is me an my bike at a recent Ducati Scrambler event&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a rider of a stylish Ducati Scrambler, I will join my local chapter of the DGR. It is a retro-themed …&lt;/p&gt;</summary><content type="html">&lt;h1&gt;I will ride like a gentlemen&lt;/h1&gt;
&lt;p&gt;On September 24th I will ride in the Distinguished Gentlemen Ride&lt;/p&gt;
&lt;p&gt;&lt;img alt="This is me an my bike at a recent Ducati Scrambler event" src="https://cdn-images-1.medium.com/max/2000/1*_2xCEsRJxtbLU8uSnI9n0w.jpeg"&gt;&lt;em&gt;This is me an my bike at a recent Ducati Scrambler event&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a rider of a stylish Ducati Scrambler, I will join my local chapter of the DGR. It is a retro-themed motorbike event, held globally in the world, to raise funds for research into prostate cancer and mental health programs as part of our mission to support men’s health globally. These funds are invested by our partners the Movember Foundation, the world’s largest men’s health organization.&lt;/p&gt;
&lt;h3&gt;PROSTATE CANCER&lt;/h3&gt;
&lt;p&gt;Prostate cancer is the second most commonly diagnosed cancer in men. The Movember Foundation is the largest funder of prostate cancer programs in the world.&lt;/p&gt;
&lt;h3&gt;SUICIDE PREVENTION | MEN’S MENTAL HEALTH&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;3 out of 4 suicides are men&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;510,000 men die from suicide globally each year. That’s one every minute. This has to change.&lt;/p&gt;
&lt;p&gt;This is my donation page. Every amount is Wellcome and will help someone in need. Thank You.
&lt;a href="https://www.gentlemansride.com/rider/SamReghenzi139106"&gt;&lt;strong&gt;Sam Reghenzi&lt;/strong&gt;
&lt;em&gt;This year I am taking part in The Distinguished Gentleman's Ride to raise funds and awareness for mens health and…&lt;/em&gt;www.gentlemansride.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;UPDATE: this is the video of my ride: enjoy!&lt;/p&gt;
&lt;center&gt;&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/4hinpxAyRhY" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/center&gt;</content><category term="blog"/><category term="motorcycle"/><category term="lifestyle"/><category term="personal"/></entry></feed>