<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jan paul</title>
    <description>The latest articles on DEV Community by jan paul (@janpauldahlke).</description>
    <link>https://dev.to/janpauldahlke</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F30712%2Ff94df703-9006-4c9c-ae91-e29ef012d70d.jpeg</url>
      <title>DEV Community: jan paul</title>
      <link>https://dev.to/janpauldahlke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janpauldahlke"/>
    <language>en</language>
    <item>
      <title>The hardest problem in my local AI agent was never the model</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:00:34 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/the-hardest-problem-in-my-local-ai-agent-was-never-the-model-3bg6</link>
      <guid>https://dev.to/janpauldahlke/the-hardest-problem-in-my-local-ai-agent-was-never-the-model-3bg6</guid>
      <description>&lt;p&gt;&lt;em&gt;How tools get picked in Eris: from grep to embeddings to grammar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Or: how a local 14B model decides which of ~50 tools to call, without me writing a giant if-else on the user's message.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Feris-system.dev%2Fblog%2Ftool-routing" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Feris-system.dev%2Fblog%2Ftool-routing" alt="original post on my blog" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I started this I knew basically nothing. How you even get an LLM to call a tool, let alone pick the right one, was not obvious to me at all, and every step was me poking at it to see what breaks. So this post is not a "here is the clean way" post. It is the honest version of that learning curve, all the way from the first clumsy version to what runs now. Tool selection in Eris went through three whole lives to get here: grepping the model's own text output, then keyword lists on the user message, then embeddings with a policy layer sitting on top. Each life existed because the one before it kept failing in a new way, and none of them made the model smarter, they just stopped me handing a small model the wrong tool.&lt;/p&gt;

&lt;p&gt;Where it landed today: the user text gets embedded, compared against one precomputed vector per tool, and a small policy layer decides whether to offer one tool, a cluster of related ones, or the whole roster. That offered list then feeds two things at once, the slim tool prompt and the GBNF grammar, so prompt and grammar can never disagree about what is callable.&lt;/p&gt;

&lt;p&gt;But it did not start there. It started with grep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grep era
&lt;/h2&gt;

&lt;p&gt;Before Eris was Eris it was &lt;a href="https://github.com/janpauldahlke/FUCKUP" rel="noopener noreferrer"&gt;FUCKUP&lt;/a&gt;. Named after Hagbard Celine's supercomputer in the &lt;em&gt;Illuminatus!&lt;/em&gt; trilogy, the First Universal Cybernetic-Kinetic Ultramicro-Programmer that answers questions from inside a golden submarine. The name doubling as a status report on the code was a happy accident, or an easterfuck. The FcpError taxonomy from back then still is alive in Eris until today. Ollama plus an Obsidian vault plus Redis plus a bash-ish tool bridge. The relevant line from that README, which I keep around as a monument:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent has &lt;strong&gt;no native tools&lt;/strong&gt;. It outputs &lt;code&gt;API: &amp;lt;command&amp;gt;&lt;/code&gt; lines. A loop parses them, calls the bridge, and feeds results back.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the real grep era, spelled out. The model printed text like &lt;code&gt;API: healthcheck&lt;/code&gt;, a shell loop grepped the output for &lt;code&gt;API:&lt;/code&gt; lines, matched the command against an allowlist, ran it through a bridge on the host, and pasted the result back into the next turn. Tool "selection" was string parsing on both ends: regex on the model output to find the call, keyword lists to decide what was even relevant to offer. It worked well enough to be dangerous. Means, it worked in the demo. In real use it did not, and you find that out live.&lt;/p&gt;

&lt;p&gt;Later it grew a second path: a patched fork of an existing orchestrator doing structured tool calling in OpenAI function-call format, because text &lt;code&gt;API:&lt;/code&gt; parsing was too brittle. And that is where the lesson landed that made me throw the whole thing away and rewrite in Rust. Local models through Ollama were awful at structured tool calls. Streaming broke &lt;code&gt;tool_calls&lt;/code&gt; and handed back raw text, custom tools did not get passed through, the system prompt sometimes never reached the model. You can patch a TypeScript orchestrator around all of that, and I did. But the deeper problem stayed.&lt;/p&gt;

&lt;p&gt;My first fix was the obvious one. If the model does not get the tool contract, then push the whole tool contract into the system prompt, hard. And it kind of worked. But the system prompt got bloated fast. Every tool with its full schema, all the time, every turn. On sparse VRAM that is exactly the wrong place to spend your tokens. The context is your budget, and I was burning it on tool definitions the model did not even need this turn.&lt;/p&gt;

&lt;p&gt;So the real fix was somewhere else. Move to llama.cpp, stop describing the tools in prose, and constrain the sampler directly instead. Hand it only the tools that matter for this turn. JIT the structured part instead of front-loading all of it. Reliability has to come from constraining the sampler, not from politely asking a streaming API to behave. That is the moment Eris starts.&lt;/p&gt;

&lt;p&gt;But back to selection. Even the keyword-matching half of the old approach, the "which tools are relevant" part, took the user line, lowercased it, and ran a pile of &lt;code&gt;contains&lt;/code&gt; checks. "weather" in the string, offer the weather tool. "remind" in the string, offer the reminder. It is the honest first thing everybody builds.&lt;/p&gt;

&lt;p&gt;Then real usage happens and it falls apart in the boring ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synonyms.&lt;/strong&gt; User says "how warm is it outside". No "weather" token anywhere. Miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other language.&lt;/strong&gt; I chat with Eris half in German. "wie spät ist es" does not contain "time".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointing.&lt;/strong&gt; "do that again", "same as before", "open it". None of these words live in any keyword list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collisions.&lt;/strong&gt; "open" matched "open the file" and "you will open the way for future AIs" with equal confidence. The second one is not a tool call, it is me being poetic at my own agent at 2am.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can patch each of these with more keywords, and then you have a 400 line lexical matcher that nobody dares touch and that still misses the next phrasing. I know because I wrote it and lived with it.&lt;/p&gt;

&lt;p&gt;Two things survived that era on purpose, and I want to be honest that they survived, because a lot of routing writeups pretend they went fully semantic and they did not.&lt;/p&gt;

&lt;p&gt;The first survivor is a set of &lt;strong&gt;lexical guards&lt;/strong&gt; for cases where a keyword is actually a hard signal. If the text has a real URL or a domain-looking token, I do not want to &lt;em&gt;hope&lt;/em&gt; the embedder feels like offering &lt;code&gt;web:fetch&lt;/code&gt;. I force it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;has_web_lexical_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thought&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;t&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;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"web:fetch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"LEXICAL_TOOL_GUARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;forced_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"web:fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;thought_preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;thought&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;"Forcing web:fetch due to lexical URL/page intent"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"web:fetch"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mf"&gt;1.0&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;Note the score &lt;code&gt;1.0&lt;/code&gt;. That is not a real cosine, it is a "do not argue with me" marker, and later the policy layer treats anything at ~0.99 and above as a forced hit that never gets demoted. The lexical checks themselves learned some manners over time. Bare &lt;code&gt;open&lt;/code&gt; used to match figurative English, so now it is phrases like &lt;code&gt;open page&lt;/code&gt;, &lt;code&gt;open the website&lt;/code&gt;, &lt;code&gt;visit the page&lt;/code&gt;, not the lone verb.&lt;/p&gt;

&lt;p&gt;The second survivor is the keyword lists themselves. They did not get deleted. They got repurposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing phrases: keywords that grew up
&lt;/h2&gt;

&lt;p&gt;The old keyword lists became &lt;strong&gt;routing phrases&lt;/strong&gt;. Same strings, roughly, but now they do a different job. Instead of being matched with &lt;code&gt;contains&lt;/code&gt;, they are the &lt;em&gt;text you embed to describe a tool&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every tool has a line of "this is what people say when they want me". For tools without a richer descriptor there is a compile-time fallback in &lt;code&gt;routing_phrases.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="s"&gt;"clock:now"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"what time is it, current time, timezone, date now, local time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"weather:current"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"weather now, temperature outside, is it raining, rainfall, sunny or cloudy, conditions today, current conditions, what's the weather like"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for the tools I cared about more, the phrases live in the TOML descriptor next to &lt;code&gt;when_to_use&lt;/code&gt; / &lt;code&gt;when_not_to_use&lt;/code&gt;, as &lt;code&gt;routing_hints&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;tool_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"agenda:remind_at"&lt;/span&gt;
&lt;span class="py"&gt;routing_hints&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"remind me at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"remind me in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"remind me about"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Descriptor hints win when present, otherwise the fallback string, otherwise just the tool description. That resolution order lives in one place so nothing drifts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;enrich_for_routing&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;descriptors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ToolDescriptorRegistry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descriptors&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="py"&gt;.routing_hints&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"{}: {}. Common triggers: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="py"&gt;.routing_hints&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;routing_phrases&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fallback_triggers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;hints&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}: {}. Common triggers: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hints&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;So the exact same phrases that used to be a brittle &lt;code&gt;contains&lt;/code&gt; list are now the seed text for a vector. Synonyms and paraphrase stop being my problem and become the embedder's problem, which is the whole point, because that is a thing embedders are actually good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embeddings now
&lt;/h2&gt;

&lt;p&gt;At startup the &lt;code&gt;ToolRouter&lt;/code&gt; embeds each tool's enriched text once and keeps the vectors in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;EmbeddingProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tool_descriptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;descriptors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToolDescriptorRegistry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;tool_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_descriptions&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tool_descriptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enrich_for_routing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descriptors&lt;/span&gt;&lt;span class="nf"&gt;.as_deref&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="nf"&gt;.embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;tool_embeddings&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&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;Same embedding model as the vector memory (&lt;code&gt;nomic-embed-text&lt;/code&gt; by default), so I am not shipping a second model just for routing. One embed model does prefetch, memory recall, and tool routing.&lt;/p&gt;

&lt;p&gt;Per turn, embed the user text, cosine against every tool vector, keep whatever clears the threshold (&lt;code&gt;tool_match_threshold&lt;/code&gt;, default &lt;code&gt;0.50&lt;/code&gt;), sort descending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;thought_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.embed_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thought&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
    &lt;span class="py"&gt;.tool_embeddings&lt;/span&gt;
    &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;thought_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emb&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;sim&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.threshold&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt;&lt;span class="nf"&gt;.partial_cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before any of this even runs there is a cheap short-input guard: greetings and tiny utterances ("hey", "thanks", "test") are treated as conversation and skip embedding entirely, unless they carry an obvious tool token like a URL or "list my documents". No point paying for an embed call and a grammar recompile to answer "hi".&lt;/p&gt;

&lt;p&gt;There is also one embarrassingly specific semantic floor worth mentioning, because it is the kind of thing you only learn from running the thing daily. Moltbook tools (my little federated social thing) kept getting weakly matched by general chat and especially by memory-recall phrasing, they all sit around 0.50 to 0.56 against each other. So unless the user actually said "moltbook" or "submolt", moltbook hits below 0.58 get dropped. Not elegant. Correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than top-k: the policy layer
&lt;/h2&gt;

&lt;p&gt;Here is the part I am actually a bit proud of, and the part most "just embed it" writeups skip.&lt;/p&gt;

&lt;p&gt;Naive routing is: take top-k by cosine, offer those, done. That is fine until you remember what happens downstream. Downstream, on the llama.cpp backend, the offered tools get compiled into a GBNF grammar that &lt;em&gt;forces&lt;/em&gt; the model's output. If the router confidently picks one wrong tool and hands the grammar exactly that one tool, the model is now structurally locked into calling the wrong thing. Top-1 with a hard grammar behind it is not "confident", it is "unrecoverable".&lt;/p&gt;

&lt;p&gt;So there is a policy layer between the raw cosine hits and the offer. It does not replace embeddings, it just vetoes, widens, or pairs. The entry point reads like a little rulebook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;RoutingSignals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;knobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RoutingPolicyKnobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RoutingDecision&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rule 1 — dialog pairing (agenda → mail → calendar → gated doc).&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;try_dialog_pairing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;
        &lt;span class="py"&gt;.embed_hits&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.partition&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;score&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;FORCED_HIT_FLOOR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 2 — lone weak embed hit demotion (+ unsure fallback).&lt;/span&gt;
    &lt;span class="c1"&gt;// Rule 3 — near-tie across related domains → affinity cluster union.&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three moves matter here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demote the lone weak hit.&lt;/strong&gt; If the only thing that cleared the threshold is a single tool sitting below &lt;code&gt;tool_single_hit_floor&lt;/code&gt; (0.58), that is not confidence, that is a coin flip. Offering it alone would GBNF-lock the model onto a guess. So a lone weak hit gets demoted and we fall through to the full roster (or, configurable, to that tool's domain cluster):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;demote_lone_weak_embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;single_hit_floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;single_hit_floor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="na"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;single_hit_floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"routing.policy.single_hit_demoted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Lone weak semantic hit demoted (avoid GBNF lock-in)"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="na"&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="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;None&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;&lt;strong&gt;Widen a near-tie into a cluster, but only for related domains.&lt;/strong&gt; When the top hits are within &lt;code&gt;match_margin&lt;/code&gt; (0.05) of each other, that is the model being torn between neighbours. "remind me tomorrow at 10" scores &lt;code&gt;clock:alarm&lt;/code&gt; and &lt;code&gt;agenda:remind_at&lt;/code&gt; almost identically, because honestly they &lt;em&gt;are&lt;/em&gt; almost the same intent. In that case I do not want to pick one and lock the grammar to it. I widen to the union of the related domain clusters and let the model choose inside the grammar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// clock + agenda + calendar share the "time" affinity bucket.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;affinity_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"agenda"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"clock"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"calendar"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"news"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"wiki"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"doc"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"memory"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"media"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"knowledge"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "only related domains" bit matters. A near-tie across &lt;code&gt;moltbook&lt;/code&gt; + &lt;code&gt;web&lt;/code&gt; + &lt;code&gt;db&lt;/code&gt; + &lt;code&gt;doc&lt;/code&gt; is not a coherent cluster, that is just mush where everything landed around 0.55. Dumping all four families' tools into the grammar would be worse than useless. So unrelated near-ties stay a plain cosine-ranked subset, no cluster dump. This one rule killed a whole class of "why did it offer the train schedule tool for a memory question" bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the ordering honest.&lt;/strong&gt; Whatever comes out, forced or cluster-widened or plain, gets re-ranked by the original cosine at the end, so the tool the embedder actually liked stays near the top of the phrase map the model reads. Cluster siblings that never got a real score inherit the best score from their domain, minus a hair, so they sort right after the seed instead of jumping the queue.&lt;/p&gt;

&lt;p&gt;There is also a small dialog memory: the last handful of successful tools is kept (capped, session-scoped) so "delete that email" after a &lt;code&gt;mail:check&lt;/code&gt; can pair to &lt;code&gt;mail:delete&lt;/code&gt; instead of being read as a bare "delete" with no object. Agenda wins over mail and calendar in those pairings, because that is the order that matched how I actually talk to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The slim tool offer
&lt;/h2&gt;

&lt;p&gt;Okay, so the policy layer produced an offered list. Now, what does the model actually see?&lt;/p&gt;

&lt;p&gt;The naive answer is: all the tools, full JSON Schema for every one, dumped into the system prompt. For ~50 tools that is a wall of schema that eats context and, worse, gives a small model 50 ways to be wrong. My earlier &lt;a href="https://eris-system.dev/blog/gbnf-grammars" rel="noopener noreferrer"&gt;GBNF post&lt;/a&gt; already argued that fewer choices means fewer mistakes, and this is the prompt-side half of that.&lt;/p&gt;

&lt;p&gt;So by default (&lt;code&gt;slim_tool_prompt = true&lt;/code&gt;) the tool-mode prompt is not the schema wall. It is two smaller things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;phrase map&lt;/strong&gt;, a little markdown table of tool name, short description, and the same routing phrases from earlier, so the model has natural-language hooks for each offered tool.&lt;/li&gt;
&lt;li&gt;The tool definitions &lt;strong&gt;with &lt;code&gt;parameters&lt;/code&gt; stripped&lt;/strong&gt;. The model sees that &lt;code&gt;vault:write&lt;/code&gt; exists and roughly what it is for, but not the full argument schema. The full schema is not needed to &lt;em&gt;decide&lt;/em&gt; to call a tool, and if the model later gets the args shape wrong, gatekeeper schema recovery supplies the real schema on demand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The phrase map is generated at runtime from the registered tools plus their descriptors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="s"&gt;"| Tool | Description (short) | Typical phrasing / triggers |"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="s"&gt;"| ---- | ------------------- | --------------------------- |"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="c1"&gt;// one row per offered tool, phrases = descriptor hints, else fallback, else description&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before that list is finalized there are a few &lt;strong&gt;offer overlays&lt;/strong&gt;, the pragmatic pairings that experience forced on me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// web:fetch or web:search in the offer? then web:find has to come too,&lt;/span&gt;
&lt;span class="c1"&gt;// because you always want to query what you just fetched.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;needs_web_find&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offered&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"web:fetch"&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"web:search"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// doc:read in the offer usually means the model will want to write a summary,&lt;/span&gt;
&lt;span class="c1"&gt;// so pair vault:write if the state allows it.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offered&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"doc:read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;offered&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"vault:write"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nn"&gt;Gatekeeper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;state_allows_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"vault:write"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;offered&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vault:write"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&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;There is also a cap (&lt;code&gt;tool_map_offer_cap&lt;/code&gt;) if you want to hard-limit how many router hits make it into the map, and a "moltbook latch" so that once a turn is clearly about moltbook, the whole moltbook family comes along instead of one lonely endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  One list, two consumers, no drift
&lt;/h2&gt;

&lt;p&gt;Here is the bit that ties the whole thing together and is the reason I bothered writing &lt;code&gt;overlays.rs&lt;/code&gt; as its own module instead of inlining it twice.&lt;/p&gt;

&lt;p&gt;The offered-tool list feeds &lt;strong&gt;two&lt;/strong&gt; consumers on every tool turn: the slim prompt assembly, and the GBNF grammar. If those two ever disagree, you get the worst kind of bug, the prompt tells the model tool X is available, the grammar forbids tool X's tokens, and the model wedges. So both call the exact same function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;slim_offered_tool_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pre_llm_matched_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;tool_map_offer_cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;moltbook_overlay_latched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gatekeeper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apply_offer_overlays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pre_llm_matched_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_map_offer_cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;moltbook_overlay_latched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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;Single source of truth. The prompt and the grammar are computed from the same &lt;code&gt;offered&lt;/code&gt;, in the same order, with the same overlays. They physically cannot drift, because there is only one list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the grammar is the whole reason this is careful
&lt;/h2&gt;

&lt;p&gt;If you only remember one thing: the reason routing is this defensive is that on llama.cpp the offer is not a suggestion, it is a hard constraint at sample time.&lt;/p&gt;

&lt;p&gt;The offered list gets compiled into a GBNF grammar. GBNF constrains which tokens the sampler is even allowed to emit, before sampling, so anything that cannot extend into a valid parse gets masked out. The model does not "try to" produce a tool call in the right shape, it &lt;em&gt;cannot&lt;/em&gt; produce anything else. Full detail is in the &lt;a href="https://eris-system.dev/blog/gbnf-grammars" rel="noopener noreferrer"&gt;GBNF post&lt;/a&gt;, but the shape is: a fixed protocol envelope (thought, status, message, tool_calls), and a &lt;code&gt;tool-call&lt;/code&gt; rule whose allowed names are exactly this turn's offered tools, with each tool's args compiled from its JSON Schema.&lt;/p&gt;

&lt;p&gt;Which is exactly why I do not let the router hand over a single confident guess. Under a hard grammar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offer one wrong tool, the model is &lt;em&gt;forced&lt;/em&gt; to call the wrong tool. No recovery inside the turn.&lt;/li&gt;
&lt;li&gt;Offer an empty tool list when the router says "this is just chat", and &lt;code&gt;tool_calls&lt;/code&gt; can only be &lt;code&gt;[]&lt;/code&gt;. The model cannot hallucinate a call. That is a feature, that is the conversational path.&lt;/li&gt;
&lt;li&gt;Offer a cluster on a near-tie, and the model gets to pick the right neighbour, but only from a small, related, structurally valid set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the grammar is what turns "structured output" from a prompt-time please into a real guarantee. The policy layer only makes sure that guarantee points at roughly the right tools. In the end there are four layers stacked on each other. Embeddings find the candidates. The policy decides how wide to open the door. The slim map tells the model what is behind it. The grammar makes sure whatever comes back actually parses. None of these layers I planned upfront. Each one is there because the one before it burned me first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone building this
&lt;/h2&gt;

&lt;p&gt;If you route tools for a local model with grammar-constrained output: the thing I got wrong the longest was to treat routing as a ranking problem. It is not really about ranking. The real question is how much you trust that ranking before you let a grammar act on it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings beat keyword matching for finding candidates. Keep your old keyword lists though, they are perfectly good embedding seeds and decent hard-override floors.&lt;/li&gt;
&lt;li&gt;Top-1 plus a hard grammar is a trap. Confidence has to be earned with a score floor and a margin check, and when it is not there, widen instead of guessing.&lt;/li&gt;
&lt;li&gt;Widen along real relationships (time-ish tools together, knowledge tools together), not along whatever happened to score near 0.55.&lt;/li&gt;
&lt;li&gt;Compute the offer once and feed both the prompt and the grammar from it. Two derivations of "what is callable" will drift, and the failure mode is nasty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this made the model smarter. It just stopped me from handing a small model a loaded grammar pointed at the wrong tool. Which, on a 14B, is most of the battle.&lt;/p&gt;

&lt;p&gt;Code is at &lt;a href="https://github.com/janpauldahlke/eris" rel="noopener noreferrer"&gt;github.com/janpauldahlke/eris&lt;/a&gt;, Apache 2.0. The full post lives on my site at &lt;a href="https://eris-system.dev/blog/tool-routing" rel="noopener noreferrer"&gt;eris-system.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Routing lives under &lt;code&gt;src/orchestrator/routing/&lt;/code&gt;, the router itself in &lt;code&gt;src/orchestrator/tool_router.rs&lt;/code&gt;. Poke at it, break it, tell me where it is dumb ;-) !!&lt;/p&gt;

&lt;p&gt;Jan&lt;/p&gt;

</description>
      <category>rust</category>
      <category>llm</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a forgetting curve for an agent with one user</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:54:40 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/i-built-a-forgetting-curve-for-an-agent-with-one-user-2668</link>
      <guid>https://dev.to/janpauldahlke/i-built-a-forgetting-curve-for-an-agent-with-one-user-2668</guid>
      <description>&lt;p&gt;&lt;strong&gt;What I learned, mostly: I overbuilt the clever part, and then kept it anyway.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;In Greek myth the dead drink from the Lethe before they cross into the underworld. River of forgetting. The water washes the old life out so the soul can move on, and the part I always liked is that forgetting there is not a flaw in the design. It is the point. Without it nothing ever gets lighter.&lt;/p&gt;

&lt;p&gt;I liked that picture a bit too much when I built memory for Eris.&lt;/p&gt;

&lt;p&gt;There is a second thing I never believed in: keeping the whole chat as "memory". Dumping every single turn into long term storage, that is not remembering, that is hoarding. Somebody has to sit down and decide, this one stays, this one can go. Human or agent, does not matter, same job. A vault should stay a place you curated and not a landfill of old transcripts.&lt;/p&gt;

&lt;p&gt;So I went and modelled human memory on purpose. Not as a cute comment somewhere in the code. As actual behaviour. Things you touch again should rise, things nobody touches should slowly fade. Decay was my Lethe, a slow drain on a float, so the agent would not drown in every little staging decision it ever made. A mention in the chat pushed the value back up a bit. For a personal agent that sits on a Markdown vault this felt serious. Grown up, even.&lt;/p&gt;

&lt;p&gt;And then the serious thing turned into a small economics model.&lt;/p&gt;

&lt;p&gt;Every staged memory carries a &lt;code&gt;promotion_score: f64&lt;/code&gt;. There is a background pass, it wakes up every two minutes, takes a fixed bite out of the score (that is the decay), and then maybe it pushes the entry one rung up the ladder, Session to Scratch to Promote, or one rung back down when the score got too low. Staging something explicitly with &lt;code&gt;memory:stage&lt;/code&gt; gives a bigger bump. Get over the threshold, longer TTL. Fall under half of the threshold that got you into your current tier, and back down you go. My own little forgetting curve. I was pretty proud of it, honestly.&lt;/p&gt;

&lt;p&gt;It compiles. Tests green. And after running this on my own vault since months, here is the summary in the short version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I never once looked at a Scratch tier score and made any decision from it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which sounds like I forgot why the numbers were there in the first place. I did not forget. The idea was never that &lt;em&gt;I&lt;/em&gt; watch a dashboard. The idea was a &lt;strong&gt;memory curation mode&lt;/strong&gt; for later: the agent, when idle or in a dedicated pass, walks its own staged memory, looks at score and tier, then decides what to keep, what to merge, what to demote, what to finally commit. The hippocampus theatre, that was the metaphor. The real reader of those floats was supposed to be the model itself, some gardener style loop that, well, does not exist yet.&lt;/p&gt;

&lt;p&gt;So right now the river runs for an observer I never got around to building. No curation mode. No walk. Thresholds are still pure gut feeling. One user on the chat path, and that user is me. So the scaffolding for a future curator already sits on top of the live agent and slows it down. That is the mess.&lt;/p&gt;

&lt;p&gt;This post is about why I should probably delete all of that, and about one simple inversion: decide less when you write, rank more when you read. Spoiler, so nobody feels cheated at the end: I did not delete it. I keep the ladder for now. More on that later. Forgetting stays good either way. I just put it in the wrong corner. If you build agent memory these days, maybe check quickly whether you are digging yourself the same hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the ladder looks like
&lt;/h2&gt;

&lt;p&gt;Eris is a local agent living on a Markdown vault. Working memory is a moka cache. The long term stuff, the notes a human should actually read, that is plain Markdown on disk. Semantic recall goes over a vector store. All fine so far. The clever bit was this one here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;EphemeralTier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// just staged, short TTL&lt;/span&gt;
    &lt;span class="n"&gt;Scratch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// "getting interesting"&lt;/span&gt;
    &lt;span class="n"&gt;Promote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// eligible for memory:commit_all&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the ladder itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EphemeralTier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Scratch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Scratch&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Promote&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Promote&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&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="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Scratch&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Promote&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Scratch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;commit_all&lt;/code&gt; flushes only the Promote entries. Session and Scratch count as "not ready yet".&lt;/p&gt;

&lt;p&gt;And this is exactly where it got annoying for the agent. The flow was clunky to begin with, first you &lt;code&gt;memory:stage&lt;/code&gt;, then later &lt;code&gt;memory:commit&lt;/code&gt; or &lt;code&gt;memory:commit_all&lt;/code&gt;. Small local models, they are not great at this multi step bookkeeping. So the model stages something genuinely useful, then tries to flush everything with &lt;code&gt;commit_all&lt;/code&gt;, and every entry that did not reach Promote yet just gets skipped, quietly. From the model's side the tool call looked like a success. Nothing landed in the vault though. So it tries again, stages again, lists the staged rows, gets confused, burns turns. I saw this happen so many times. The ladder should have kept junk out of the vault. In practice it mostly kept the agent from getting the job done.&lt;/p&gt;

&lt;p&gt;Scoring on each pass, roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// every promotion tick&lt;/span&gt;
&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.promotion_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.promotion_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.promotion_decay_per_tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.promotion_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.needs_review&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.tier&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_tier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="nf"&gt;.ttl_for_tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_tier&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="c1"&gt;// demotion: score fell below 0.5 x the threshold that got us here&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.promotion_score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold_to_current&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="py"&gt;.tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_tier&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;And then the config kept growing. Three TTLs. Two thresholds. Decay per tick, an eval interval, a mention boost, a stage boost. Six knobs, more actually, for a subsystem I was never even looking at. Should have been the smell already, right there.&lt;/p&gt;

&lt;p&gt;There is one detail that is almost a bit funny. The stage boost is 3.5, the threshold from Session to Scratch is 3.0. Which means one single &lt;code&gt;memory:stage&lt;/code&gt; already gets you over the first rung on the next pass. Scratch happened more or less by itself. I had built a step that nobody ever really stands on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How scores go up, and why it breaks
&lt;/h2&gt;

&lt;p&gt;Decay was the Lethe half. The remembering half was, in the end, two bumps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Explicit &lt;code&gt;memory:stage&lt;/code&gt; adds &lt;code&gt;promotion_stage_boost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;After a user turn a mention hook walks over the staged rows. When the user text "covers" the row's &lt;code&gt;canonical_key&lt;/code&gt;, it adds &lt;code&gt;promotion_mention_boost&lt;/code&gt;, bumps &lt;code&gt;mention_count&lt;/code&gt;, refreshes the TTL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the second one sounds semantic. It really is not. No nomic-embed in there, no cosine, nothing. The code takes the user message, cuts it into words of length three and up, and checks how many overlap with the tokens from the key. &lt;code&gt;hagbard_loves_piano&lt;/code&gt; becomes &lt;code&gt;hagbard&lt;/code&gt;, &lt;code&gt;loves&lt;/code&gt;, &lt;code&gt;piano&lt;/code&gt;. Two token hits, or one that is long enough on its own, and the score climbs. The whole thing fits on one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// True if user message words match this row strongly enough (deterministic, no embeddings).&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;user_covers_canonical_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;canonical_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canonical_key&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&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;tokens&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0usize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;longest_hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0usize&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;t&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tokens&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;user_words&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;hits&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;longest_hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;longest_hit&lt;/span&gt;&lt;span class="nf"&gt;.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.len&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;longest_hit&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, as long as you keep repeating the words that happen to live in the title slug, the row goes up. In that one narrow case it works. It breaks the second real language shows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synonyms.&lt;/strong&gt; You staged &lt;code&gt;coffee_preference&lt;/code&gt;, and the user says "espresso order" or "my usual drink". Overlap zero. No boost. The thing you are so obviously still talking about decays like you dropped it completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointing.&lt;/strong&gt; Real chat is full of this, "keep that", "commit it", "same as before", "write this into the vault". None of these words ever live in a &lt;code&gt;canonical_key&lt;/code&gt;, so the hook basically cannot fire. Exactly in the moments where you point at a staged row most clearly, you miss the bump. Brittle is the friendly word for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paraphrase.&lt;/strong&gt; Same topic, other words, score stays flat, the pass keeps eating. The tier can demote while the conversation is literally still about that note.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this "forgetting curve" did not measure importance at all. It measured string overlap against a slug that I myself invented at stage time. Repeat the slug, score up. Talk like a normal person, with pointers and synonyms, and the bumps never arrive. And here is the annoying part: I already had nomic-embed in the stack, for vault prefetch and for tool routing. I left it out of promotion on purpose, for determinism. That call by itself was fine. What was not fine is that I then used this brittle string thing as if it were a hippocampus.&lt;/p&gt;

&lt;p&gt;The curation mode was supposed to read these scores later on. I am honestly glad it never shipped on top of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one concurrency exception
&lt;/h2&gt;

&lt;p&gt;I have a hard rule in this codebase. No shared mutable state across threads. Orchestrator and UI talk over &lt;code&gt;mpsc&lt;/code&gt;. No &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; just because it happens to be convenient.&lt;/p&gt;

&lt;p&gt;Except for one single &lt;code&gt;AtomicBool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Snapshot daemon and orchestrator share &lt;code&gt;promotion_suppressed_during_step&lt;/code&gt;. While &lt;code&gt;step()&lt;/code&gt; is running, the LLM round trip, the tool batch, all the slow parts, the daemon is not allowed to decay or to promote. Otherwise you get these lovely races, "the model is about to commit this row" versus "decay just demoted it". So the orchestrator flips a flag, the daemon reads it and skips its tick.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// When true, skip evaluate_promotions_and_decay so decay/tier moves&lt;/span&gt;
&lt;span class="cd"&gt;/// do not race a slow LLM or tool batch.&lt;/span&gt;
&lt;span class="n"&gt;promotion_suppressed_during_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AtomicBool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made an exception to my own concurrency rule, so that a forgetting curve I do not even look at can interleave "safely" with the chat. When you arrive at a point like that, the design is usually already wrong somewhere, and throwing more locking at it will not save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it was actually for
&lt;/h2&gt;

&lt;p&gt;Honestly? Two jobs got glued into one.&lt;/p&gt;

&lt;p&gt;Short term the ladder was a write time gate. Do not let junk flow into the vault by itself, only the "important" things should turn into Markdown, and important means the score, built up over time.&lt;/p&gt;

&lt;p&gt;Long term those same scores and tiers were meant as the input for the curation mode from above. Agent walks the memory, reads the ladder state, chooses. I built the sensors first and the walker never came. Either way it sounds scientific. Mostly it is a prior with extra code around it, plus a futures contract I never once cashed in.&lt;/p&gt;

&lt;p&gt;And without the ladder I already had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit &lt;code&gt;memory:commit&lt;/code&gt; and &lt;code&gt;memory:commit_all&lt;/code&gt;, the model or me decides&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;needs_review&lt;/code&gt; flag that stops auto promotion, which for now I flip by hand, there is no detector yet&lt;/li&gt;
&lt;li&gt;turn end mention handling, a key shows up again, interest bumps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was already enough to stop the vault from becoming a dump. The score ladder was a third mechanism on a door that had two locks on it already. Plus it pretended to know the future.&lt;/p&gt;

&lt;p&gt;And that is the part that bugs me now. At write time you simply do not know if a thing will matter in three weeks. You guess. Embeddings are far better at a different question: given what the user just said right now, what from the past is relevant? That is a read time question. And that one you can actually compute.&lt;/p&gt;

&lt;p&gt;So in function I need two states, not three tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Staged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;working memory, has a TTL, listable, can be committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Committed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Markdown in the vault, indexed for semantic recall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scratch was not needed. The Promote only gate on &lt;code&gt;commit_all&lt;/code&gt; was worse than not needed, because it stalled the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What retrieval was already doing anyway
&lt;/h2&gt;

&lt;p&gt;While the daemon was busy decaying its floats, the turn start prefetch was already ranking the vault memory by similarity and dropping a small block into the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[RELEVANT_LEARNED_MEMORY]
...snippets the embedder thinks match this turn...
[/RELEVANT_LEARNED_MEMORY]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No tiers. No ladder scores. Query now, rank now, stay inside a character budget, done. Junk that never matches any query just sits there and hurts nobody. I should have trusted this a lot earlier.&lt;/p&gt;

&lt;p&gt;So if I ever pull the trigger, the delete list is fairly obvious. Call it one big TODO:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scratch tier, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;prev&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, the whole ladder&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;promotion_score&lt;/code&gt;, the decay pass, the demotion logic&lt;/li&gt;
&lt;li&gt;most of the config knobs, keep one &lt;code&gt;staged_ttl_secs&lt;/code&gt; and be done&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;AtomicBool&lt;/code&gt;, it only exists because of the ladder anyway&lt;/li&gt;
&lt;li&gt;the Promote only gate on &lt;code&gt;commit_all&lt;/code&gt;, so it would commit all staged rows minus the contradicted ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would stay: the tools the model already knows (&lt;code&gt;stage&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;commit_all&lt;/code&gt;, &lt;code&gt;staged_list&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;), the &lt;code&gt;needs_review&lt;/code&gt; flag, the snapshot daemon but only as persistence, and the mention handling, probably just refreshing the TTL instead of feeding a fake score.&lt;/p&gt;

&lt;p&gt;A couple hundred lines gone. One concurrency exception gone. Same capabilities. And a lot less to explain to myself the next time I open that file. On paper it is a clean win. Keep that "on paper" in mind, we come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper inversion, and Markdown stays
&lt;/h2&gt;

&lt;p&gt;Once you stop over curating at write time, another design gets kind of obvious.&lt;/p&gt;

&lt;p&gt;You can append cheap episodic records, turn digests, tool outcomes, mentions, without first deciding whether they are "Promote material" or not. At this scale storage is basically free. You filter at retrieval instead, similarity times recency, maybe some BM25 on top. The curated layer stays what it is supposed to be, human readable Markdown in the vault. The index is a derived thing. Delete the index, rebuild it from the vault on boot. That property is not up for discussion, not for me. A binary agent memory file, fine, let it hold the index. It must never become the source of truth.&lt;/p&gt;

&lt;p&gt;Small note to myself: do not go and tune the ladder instead of deleting it. Making machinery that nobody looks at more precise, that is just polishing the overengineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zettelkasten, a graph I prepared and then never really walked
&lt;/h2&gt;

&lt;p&gt;There is a second ghost in this whole design, Niklas Luhmann's Zettelkasten.&lt;/p&gt;

&lt;p&gt;Short version for the people who do not live in that world. Luhmann, a German sociologist, built this massive slip box of notes. Every note small, atomic, linked by hand to other notes. And the power was never "save everything". It was a growing graph of ideas that you could walk through. A new note had to find its neighbours first. The thinking happened inside the links, as much as inside the cards. People in the PKM and Obsidian corner still chase exactly this shape, and for good reasons.&lt;/p&gt;

&lt;p&gt;I wanted the vault to become that, for the agent. Stable &lt;code&gt;node_id&lt;/code&gt;s, revisioned Markdown commits, folders like Topology, Discourse, Synthesis, contradiction as a thing you mark instead of silently overwriting it. The layout is more or less ready to turn into a graph.&lt;/p&gt;

&lt;p&gt;We are just not there yet.&lt;/p&gt;

&lt;p&gt;What I actually run day to day is flatter than all this: stage, sometimes commit, semantic search over the chunks, a handful of snippets prefetched into the prompt. The graph dream sat right next to the forgetting curve dream. Both of them were "human memory, but engineered". Both made the agent path heavier, before the simple loop was even boringly reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is an Eris story, and an overambition story
&lt;/h2&gt;

&lt;p&gt;Eris is my local first Rust agent. Vault as memory, tools behind a gatekeeper, llama.cpp with GBNF so the JSON protocol is already constrained at sample time, a TUI and a web frontend hanging off the same orchestrator.&lt;/p&gt;

&lt;p&gt;I put a lot of hours into making small local models behave like they had a real protocol. Grammars, recovery budgets, an LLM only view of the transcript for the llama-server templates. And memory was the exact spot where I quietly got soft again. A float, a threshold, a story about importance that sounded like neuroscience, and on top a Zettelkasten shaped vault for a graph I never really walked. The agent did not get smarter from any of it. The code got heavier. The &lt;code&gt;AtomicBool&lt;/code&gt; was the receipt for that.&lt;/p&gt;

&lt;p&gt;If you build agent memory and you catch yourself doing Session, Scratch, long term, with decay "like a human hippocampus", stop for a second and ask the boring question. Who reads the scores? If the answer is nobody, then you are not building a forgetting curve. You are building config debt and telling yourself a nice myth about it.&lt;/p&gt;

&lt;p&gt;Here comes the uncomfortable bit, plainly. I overengineered. I wanted too much at once, Lethe style decay, Promote gates, a future agent that curates by score, Luhmann links, the whole cathedral. And I did not even seriously try the things other people already shipped, or at least sketched out, in this space. Memvid. Mempalace. Karpathy's wiki style memory. Probably three more approaches I bookmarked and then ignored, while I polished my own knobs. For somebody who keeps claiming he cares about deletion, that is a weak look.&lt;/p&gt;

&lt;p&gt;So the sane next move would be simple. Delete the ladder. Keep staged versus committed. Keep choosing what to remember instead of dumping the whole chat into a file. Keep Markdown as the source of truth. If the agent later walks its own memory it can lean on retrieval, contradictions, plain note text. It does not need my homemade hippocampus floats for that.&lt;/p&gt;

&lt;p&gt;Would. And here is the honest part I owe you as a solo dev: I am not doing it. Not this week anyway. There is now a fat &lt;code&gt;// TODO: kill the ladder&lt;/code&gt; sitting in the repo, and I fully expect to walk past it every day and leave it alone. Eris is a hobby. I build it for the pure joy of building it, and the ladder is somehow my little darling even while I write a whole post about why it is wrong. So it stays, on probation.&lt;/p&gt;

&lt;p&gt;And honestly the ladder is not even the loudest voice in my head. The real solo dev problem is that I cannot decide where to go next, because everything sounds more fun than removing a &lt;code&gt;f64&lt;/code&gt;. Put the chat sessions into SQLite and let the agent query its own history with real SQL. A proper sandbox so it can write and run code without me hovering over the process. Actual builds I can hand to other people, instead of "clone the repo and pray". A web based &lt;code&gt;LlmEngine&lt;/code&gt; backend, OpenRouter and the like, so Eris is not chained to my local llama.cpp forever. Every single one of these is more exciting than a cleanup, and that is exactly why the ladder is still alive.&lt;/p&gt;

&lt;p&gt;Forgetting stays good. I still like the Lethe image a lot. I only stop pretending that a &lt;code&gt;f64&lt;/code&gt; on a daemon tick ever was that river. The scores can keep ticking in the background for now, I just do not tell myself a neuroscience story about them anymore. TTL expiry, an explicit commit, ranking at read time, that already forgets plenty on its own, without a fake observer watching over it.&lt;/p&gt;

&lt;p&gt;Two states would be enough. Explicit commit. Rank when you read. Notes in plain text. Ambition very much not on a leash, if we are being honest.&lt;/p&gt;

&lt;p&gt;One day I will delete the ladder. Nobody would miss the scores, nobody reads them. But not today. Today there is a SQLite branch calling, and that is simply more fun.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo: &lt;a href="https://github.com/janpauldahlke/eris" rel="noopener noreferrer"&gt;github.com/janpauldahlke/eris&lt;/a&gt; · site: &lt;a href="https://eris-system.dev" rel="noopener noreferrer"&gt;eris-system.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>aimemory</category>
      <category>rust</category>
    </item>
    <item>
      <title>How I use GBNF grammars to make small local models stop hallucinating JSON</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:22:43 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/how-i-use-gbnf-grammars-to-make-small-local-models-stop-hallucinating-json-46g</link>
      <guid>https://dev.to/janpauldahlke/how-i-use-gbnf-grammars-to-make-small-local-models-stop-hallucinating-json-46g</guid>
      <description>&lt;p&gt;&lt;em&gt;Or: why your 8B model keeps breaking your agent's tool protocol, and what you can do about it at the token level.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://eris-system.dev/blog/gbnf-grammars" rel="noopener noreferrer"&gt;eris-system.dev/blog/gbnf-grammars&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been building &lt;a href="https://github.com/janpauldahlke/eris" rel="noopener noreferrer"&gt;Eris&lt;/a&gt;, a local-first AI agent written in Rust. It sits on your machine, reads and writes an Obsidian-compatible Markdown vault, and calls tools (memory, reminders, web fetch, email, calendar) through a structured JSON protocol. The LLM backend is llama.cpp, the models are 8-26B GGUFs, and nothing leaves your machine unless you explicitly opt in.&lt;/p&gt;

&lt;p&gt;The single hardest engineering problem in the whole project wasn't the orchestrator, the TUI, or the semantic memory layer. It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting a small local model to reliably emit valid, schema-conformant JSON.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about how I solved it with GBNF grammars (compiled per-session and narrowed per-turn) and why I think this approach generalises to anyone building tool-calling agents on local models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: small models are sloppy with JSON
&lt;/h2&gt;

&lt;p&gt;If you've tried to build an agent on a local 8B or even 12B model, you know the drill. You tell the model to respond in JSON. It does, mostly. Then on turn 47 it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgets a closing brace&lt;/li&gt;
&lt;li&gt;Adds a trailing comma in an array&lt;/li&gt;
&lt;li&gt;Invents a tool name that doesn't exist (&lt;code&gt;vault_read&lt;/code&gt; instead of &lt;code&gt;vault:read&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Wraps its JSON in a markdown code fence&lt;/li&gt;
&lt;li&gt;Emits valid JSON followed by a paragraph of conversational text&lt;/li&gt;
&lt;li&gt;Returns &lt;code&gt;"args": "path=notes/today.md"&lt;/code&gt; instead of &lt;code&gt;"args": {"path": "notes/today.md"}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollama's &lt;code&gt;format: json&lt;/code&gt; helps a bit. It constrains output to &lt;em&gt;some&lt;/em&gt; valid JSON, but it says nothing about &lt;em&gt;which&lt;/em&gt; JSON. Your tool protocol schema isn't enforced. The model can return &lt;code&gt;{"sure": "here is your file"}&lt;/code&gt; and that's perfectly valid JSON. It just isn't a valid tool call.&lt;/p&gt;

&lt;p&gt;OpenAI and Anthropic solve this with server-side function-calling APIs. But we're running locally. We don't have that luxury. We have something better.&lt;/p&gt;

&lt;h2&gt;
  
  
  GBNF: constraining generation at the token level
&lt;/h2&gt;

&lt;p&gt;llama.cpp supports &lt;a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md" rel="noopener noreferrer"&gt;GBNF grammars&lt;/a&gt;, a BNF-like format that constrains which tokens the sampler is allowed to emit. If a token would produce a string that can't be extended into a valid parse of the grammar, it gets masked out &lt;em&gt;before&lt;/em&gt; sampling. The model literally cannot generate malformed output. Not 'usually doesn't'. Cannot.&lt;/p&gt;

&lt;p&gt;This is a fundamentally different thing from prompt-engineering the model to 'please respond in JSON'. It's a hard constraint on the sampler, enforced at every single token position.&lt;/p&gt;

&lt;p&gt;Eris compiles a GBNF grammar at session start that defines the exact shape of its protocol envelope. Here's the skeleton from &lt;code&gt;src/engine/grammar/envelope.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;STATIC_GRAMMAR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;r#"root ::= "{" ws thought-kv "," ws status-kv "," ws message-kv "," ws toolcalls-kv ws "}"

ws ::= [ \t\n]*

thought-kv ::= "\"thought\"" ws ":" ws json-string
status-kv  ::= "\"status\"" ws ":" ws status-enum
message-kv ::= "\"message_to_user\"" ws ":" ws (json-string | "null")
toolcalls-kv ::= "\"tool_calls\"" ws ":" ws "[" ws tool-call-list ws "]"

status-enum ::= "\"Task\"" | "\"Reflect\"" | "\"Idle\"" | "\"Process\""
"#&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every LLM response &lt;em&gt;must&lt;/em&gt; be a JSON object with exactly four keys: &lt;code&gt;thought&lt;/code&gt; (chain-of-thought, always present), &lt;code&gt;status&lt;/code&gt; (one of four enum values), &lt;code&gt;message_to_user&lt;/code&gt; (string or null), and &lt;code&gt;tool_calls&lt;/code&gt; (array of name+args objects). The model can't deviate. It can't add a fifth key. It can't misspell &lt;code&gt;tool_calls&lt;/code&gt;. It can't emit a status value that doesn't exist in the enum.&lt;/p&gt;

&lt;p&gt;The tool call shape itself gets appended depending on whether we're in static mode (any JSON object for args) or dynamic mode (per-tool typed args):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Per-tool dynamic-args version: tool-call dispatches into `tool-with-args`.&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TOOL_CALL_RULES_DYNAMIC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;r#"tool-call-list ::= "" | tool-call ("," ws tool-call)*
tool-call ::= "{" ws "\"name\"" ws ":" ws tool-with-args ws "}"
"#&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;tool-with-args&lt;/code&gt; here. That's where it gets interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling JSON Schema to GBNF rules
&lt;/h2&gt;

&lt;p&gt;Tool arguments are where small models really struggle. A &lt;code&gt;vault:read&lt;/code&gt; call needs &lt;code&gt;{"relative_path": "some/file.md"}&lt;/code&gt;. A &lt;code&gt;web:fetch&lt;/code&gt; call needs &lt;code&gt;{"url": "https://..."}&lt;/code&gt;. Without constraints, the model will happily call &lt;code&gt;vault:read&lt;/code&gt; with &lt;code&gt;{"file": "notes.md"}&lt;/code&gt; (wrong key name, schema validation fails, recovery turn wasted).&lt;/p&gt;

&lt;p&gt;Eris goes further: it compiles each tool's JSON Schema into GBNF rules at startup. The schemas come from Rust structs via &lt;code&gt;schemars&lt;/code&gt;, so the types are the source of truth. Here's the compiler entry point in &lt;code&gt;src/engine/grammar/schema_to_gbnf.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;schema_to_gbnf_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;RootSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GbnfRule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rule_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool_name_to_rule_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CompileCtx&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;definitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="py"&gt;.definitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra_rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compile_schema_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="py"&gt;.schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rule_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;rule_name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="nf"&gt;.extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.extra_rules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;rule_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&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;A &lt;code&gt;GbnfRule&lt;/code&gt; is just a &lt;code&gt;(String, String)&lt;/code&gt; tuple: rule name and rule body. The compiler walks the JSON Schema tree and emits GBNF productions for each type it finds. Objects become key-value sequences with required fields unconditional and optional fields wrapped in &lt;code&gt;(...)?&lt;/code&gt;. Strings with enums become GBNF alternations. Arrays get a helper list rule. &lt;code&gt;$ref&lt;/code&gt; pointers get resolved against the definitions map.&lt;/p&gt;

&lt;p&gt;The object compiler is the meatiest part. It sorts properties (required first, then optional, alphabetically within each group) and emits them with proper comma handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compile_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ObjectValidation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parent_rule_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;CompileCtx&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... validation checks, depth guard (MAX_DEPTH = 2) ...&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;required_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;
        &lt;span class="py"&gt;.required&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Sort: required first (alphabetically), then optional (alphabetically).&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;all_fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FieldInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;required_fields&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optional_fields&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.copied&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_fields&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"\\\"&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;\\\"\"&lt;/span&gt;&lt;span class="s"&gt; ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="py"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="py"&gt;.expr&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;i&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="py"&gt;.required&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"({kv} )?"&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="py"&gt;.required&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws {kv}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws {kv} )?"&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws {} ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;So for a Rust struct like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(JsonSchema,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;VaultWriteArgs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;relative_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WriteMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// enum: "overwrite" | "append"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler produces GBNF rules roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vault-write-args ::= "{" ws "\"content\"" ws ":" ws json-string
                      ws "," ws "\"mode\"" ws ":" ws ("\"overwrite\"" | "\"append\"")
                      ws "," ws "\"relative_path\"" ws ":" ws json-string ws "}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model emits &lt;code&gt;"vault:write"&lt;/code&gt; as the tool name, the sampler &lt;em&gt;forces&lt;/em&gt; the args to conform to &lt;code&gt;vault-write-args&lt;/code&gt;. The model can't use the wrong key. It can't pass a number where a string is required. It can't use a mode value that isn't &lt;code&gt;overwrite&lt;/code&gt; or &lt;code&gt;append&lt;/code&gt;. The schema is literally part of the grammar.&lt;/p&gt;

&lt;p&gt;Arrays work too. For a tool with &lt;code&gt;tags: Vec&amp;lt;String&amp;gt;&lt;/code&gt;, the compiler emits a helper list rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compile_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ArrayValidation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;parent_rule_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;CompileCtx&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;items_expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="py"&gt;.items&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SingleOrVec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_schema&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;item_schema&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;item_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{parent_rule_name}-item"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nf"&gt;compile_schema_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;item_rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"json-value"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"json-value"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nn"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"json-value"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;list_rule_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{parent_rule_name}-list"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;list_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{items_expr} (&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws {items_expr})*"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.extra_rules&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;list_rule_name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;list_body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; ws ({list_rule_name})? ws &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&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;Which produces something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test-array-args-tags-list ::= json-string ("," ws json-string)*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The depth guard (&lt;code&gt;MAX_DEPTH = 2&lt;/code&gt;) prevents the compiler from chasing recursive schemas forever. If a schema is too complex (deeply nested &lt;code&gt;oneOf&lt;/code&gt;, recursive types, free-form &lt;code&gt;additionalProperties&lt;/code&gt; with no fixed keys), the compiler returns &lt;code&gt;None&lt;/code&gt; and the tool falls back to a generic &lt;code&gt;json-object&lt;/code&gt; rule. Graceful degradation, not a crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-turn grammar narrowing: the real trick
&lt;/h2&gt;

&lt;p&gt;Here's the thing that really moves the needle for small models: Eris doesn't give the model the full 50-tool grammar on every turn.&lt;/p&gt;

&lt;p&gt;Before each LLM call, the orchestrator runs a &lt;strong&gt;ToolRouter&lt;/strong&gt;, a semantic similarity search using the same embedding model as vector memory. The user's message gets embedded and compared against precomputed vectors for each tool (built from tool names, descriptions, and routing hints). Only tools above a cosine similarity threshold make the cut.&lt;/p&gt;

&lt;p&gt;The GBNF grammar is then recompiled to include &lt;em&gt;only those tools&lt;/em&gt;. If the user says 'what time is it', the grammar's &lt;code&gt;tool-with-args&lt;/code&gt; alternation might contain only &lt;code&gt;clock:now&lt;/code&gt;. The model can't call &lt;code&gt;vault:write&lt;/code&gt; even if it wanted to. Those tokens simply aren't available to the sampler.&lt;/p&gt;

&lt;p&gt;The subset cache lives in &lt;code&gt;src/orchestrator/core/llama_gbnf_subset.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_or_compile_subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gatekeeper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tool_names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_names&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="nf"&gt;.sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CACHE_KEY_NO_TOOLS&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1e&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// ASCII record separator as cache key&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.inner&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map_err&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;FcpError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;EngineFault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GBNF subset cache mutex poisoned"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Cache miss: compile grammar for exactly these tools&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToolGrammarEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;per_tool_rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gatekeeper&lt;/span&gt;
                &lt;span class="nf"&gt;.parameters_root_schema_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;schema_to_gbnf_rule&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;_rule_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ToolGrammarEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;per_tool_rules&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="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compile_fcp_envelope_grammar_dynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compiled&lt;/span&gt;&lt;span class="nf"&gt;.into_boxed_str&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arc&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arc&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;Same tool set on a different turn? Cache hit, no recompilation. The &lt;code&gt;Arc&amp;lt;str&amp;gt;&lt;/code&gt; keeps the compiled grammar around cheaply.&lt;/p&gt;

&lt;p&gt;And here's how the orchestrator picks which grammar to use on each LLM call, from the main cognitive loop in &lt;code&gt;src/orchestrator/core/step.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grammar_override&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_session_grammar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.config&lt;/span&gt;&lt;span class="nf"&gt;.is_llamacpp&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="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Ollama: no grammar support, skip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tools_needed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No tools matched this turn: compile empty grammar (forces tool_calls: [])&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gbnf_subset_cache&lt;/span&gt;
        &lt;span class="nf"&gt;.get_or_compile_subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;targeted_tools&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Recovery/retry: only the specific tools we're retrying&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;targeted_tools&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gbnf_subset_cache&lt;/span&gt;
        &lt;span class="nf"&gt;.get_or_compile_subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;slim_assembly&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Normal path: pre-LLM router's top-K matched tools&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;offered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;slim_offered_tool_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pre_llm_matched_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.tool_map_offer_cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;moltbook_overlay_latched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.state&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;offered&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&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="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// fall back to full session grammar&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gbnf_subset_cache&lt;/span&gt;
            &lt;span class="nf"&gt;.get_or_compile_subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gatekeeper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;offered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;true&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// full tool roster, session grammar&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four branches, four different grammar scopes. The &lt;code&gt;!tools_needed&lt;/code&gt; case is fun: when the router decides the user is just chatting (no tools relevant), the grammar still enforces the protocol envelope but compiles with an empty tool list. That means &lt;code&gt;tool_calls&lt;/code&gt; can only be &lt;code&gt;[]&lt;/code&gt;. The model simply cannot hallucinate a tool call when the router says 'this is just conversation'.&lt;/p&gt;

&lt;p&gt;This has two effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reliability goes way up.&lt;/strong&gt; Fewer choices means fewer mistakes. An 8B model choosing between 3 tools is dramatically more reliable than one choosing between 50.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation is faster.&lt;/strong&gt; A smaller grammar means less work for the constrained sampler at each token position.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The gatekeeper: defence in depth
&lt;/h2&gt;

&lt;p&gt;Grammar enforcement is necessary but not sufficient. Even with a perfect grammar, the model can still call a tool with semantically wrong arguments (a valid path that doesn't exist, or an action that's not appropriate in the current agent state).&lt;/p&gt;

&lt;p&gt;Eris layers a &lt;strong&gt;Gatekeeper&lt;/strong&gt; on top (&lt;code&gt;src/tools/gatekeeper.rs&lt;/code&gt;). Every tool call passes through it before execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. State-based authorization&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;state_allows_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FcpError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToolFault&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tool not authorized in state {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Normalize common LLM arg-naming mistakes before validation&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_tool_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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. Runtime JSON Schema validation (belt and suspenders with grammar)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;compiled_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;JSONSchema&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;schema_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;FcpError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compiled_schema&lt;/span&gt;&lt;span class="nf"&gt;.validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FcpError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;SchemaViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Execute&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="nf"&gt;.execute&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="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Semantic guard: empty results in Recover state are suspicious&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Recover&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FcpError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToolFault&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Semantic Guard: Tool returned zero logic results during recovery"&lt;/span&gt;
                &lt;span class="nf"&gt;.to_string&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="nf"&gt;Ok&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arg normalization is worth a mention. Small models frequently use slightly wrong key names: &lt;code&gt;q&lt;/code&gt; instead of &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt; instead of &lt;code&gt;relative_path&lt;/code&gt;, &lt;code&gt;top_n&lt;/code&gt; instead of &lt;code&gt;max_headlines&lt;/code&gt;. Rather than wasting a recovery turn, the gatekeeper silently maps common aliases before validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normalize_tool_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="nf"&gt;.as_object_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&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;tool_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"web:search"&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;alias&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"search_query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"search"&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="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.contains_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;val&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="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.retain&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;WEB_SEARCH_KEYS&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... similar blocks for web:fetch, web:find, news:today,&lt;/span&gt;
    &lt;span class="c1"&gt;//     vision:see, media:catalog (path -&amp;gt; relative_path) ...&lt;/span&gt;
    &lt;span class="n"&gt;args&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 a pragmatic concession. The grammar enforces &lt;em&gt;structure&lt;/em&gt;. The normalizer handles &lt;em&gt;naming drift&lt;/em&gt;. The schema validator catches whatever slips through both.&lt;/p&gt;

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

&lt;p&gt;On the Ollama backend (no grammar), a long session with a 12B model hits about 3-5% protocol failures per turn. Malformed JSON, invented tool names, trailing prose after the JSON object. Each failure costs a recovery turn (re-prompt with the parse error), which burns context window and adds latency.&lt;/p&gt;

&lt;p&gt;On the llama.cpp backend with GBNF, protocol failures drop dramatically. Short to medium sessions are essentially clean. But I won't claim zero failures: on long contexts where the model starts losing coherence, even grammar-constrained output can degrade. The grammar guarantees &lt;em&gt;syntactic&lt;/em&gt; validity (the JSON will parse, the keys will be right, the enum values will exist), but it can't fix a model that fills every string field with garbage or repeats the same tool call in a loop because it's lost the plot at 28k tokens.&lt;/p&gt;

&lt;p&gt;That's why Eris doesn't rely on the grammar alone. There's a bounded recovery loop that catches schema violations, empty tool results, and semantic nonsense, feeds the model a concrete correction, and retries with a capped budget. The grammar killed the easy failures (malformed JSON, invented tool names). The recovery loop handles the rest. I'll write more about that mechanism in a follow-up post.&lt;/p&gt;

&lt;p&gt;The difference in session stability is still massive. A 100-turn session on Ollama reliably degrades; the same session on llama.cpp with grammar stays usable far longer. Just don't let anyone tell you it's a silver bullet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounded generation: the other half
&lt;/h2&gt;

&lt;p&gt;Even with a perfect grammar, generation can go wrong if the model fills its context window mid-response. llama.cpp will truncate, and your valid-so-far JSON becomes invalid because the closing braces never got emitted.&lt;/p&gt;

&lt;p&gt;Eris sets &lt;code&gt;n_predict&lt;/code&gt; (max tokens to generate) per call, calibrated to leave room in the context window for the response to actually finish. The grammar ensures structure; bounded generation ensures the structure has room to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, briefly
&lt;/h2&gt;

&lt;p&gt;For context on where the grammar fits in the bigger picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Rust binary.&lt;/strong&gt; ~270 source files, ~50 tools, three presentation surfaces (ratatui TUI, localhost web UI via Axum+SSE, optional Discord sidecar), all sharing one orchestrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown vault as memory.&lt;/strong&gt; Notes in an Obsidian-compatible directory tree. Tiered memory: ephemeral staging (in-process), committed (written to vault), semantic recall (Qdrant vector search over vault chunks). Turn-start prefetch embeds the user message and injects relevant memories into the system prompt before the LLM even runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No unsafe, no panics, no println.&lt;/strong&gt; Production code uses &lt;code&gt;?&lt;/code&gt; propagation through a typed error taxonomy (&lt;code&gt;FcpError&lt;/code&gt;). Diagnostics go through &lt;code&gt;tracing&lt;/code&gt; to rotating log files under &lt;code&gt;.fcp/telemetry/&lt;/code&gt;. Never to the TUI buffer. &lt;code&gt;println!&lt;/code&gt; would corrupt the ratatui render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actor model concurrency.&lt;/strong&gt; No &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; shared state between threads (well, except the grammar cache, which is a cold path). Communication via &lt;code&gt;tokio::sync::mpsc&lt;/code&gt; channels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should you do this?
&lt;/h2&gt;

&lt;p&gt;If you're building an agent on local models and you need structured output: yes, absolutely. GBNF grammar enforcement is the single biggest reliability win I've made on this project. More impactful than prompt engineering, more impactful than model selection, more impactful than retry logic.&lt;/p&gt;

&lt;p&gt;The key insight is that &lt;strong&gt;the grammar is not a prompt.&lt;/strong&gt; It's not a suggestion. It's a constraint on the sampler. The model can't violate it any more than it can output a token that doesn't exist in its vocabulary. That's a qualitative difference from anything you can do with system prompts.&lt;/p&gt;

&lt;p&gt;The tradeoff: you're coupled to llama.cpp (or any sampler that supports grammars; VLLM has experimental support, Outlines does it differently). You need to write or compile the grammar. And very complex schemas (recursive types, polymorphic unions) may not compile cleanly. But for the typical agent tool protocol (an envelope with a discriminated union of tool calls) it works beautifully.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/janpauldahlke/eris
&lt;span class="nb"&gt;cd &lt;/span&gt;eris &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6333:6333 &lt;span class="nt"&gt;-p&lt;/span&gt; 6334:6334 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; eris-qdrant-data:/qdrant/storage qdrant/qdrant
./target/release/eris chat   &lt;span class="c"&gt;# first run launches the setup wizard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is at &lt;a href="https://github.com/janpauldahlke/eris" rel="noopener noreferrer"&gt;github.com/janpauldahlke/eris&lt;/a&gt;. The project site is &lt;a href="https://eris-system.dev" rel="noopener noreferrer"&gt;eris-system.dev&lt;/a&gt;. Apache 2.0. Stars and contributions welcome.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;If you're working on local AI agents, structured generation, or Rust+LLM integration, reach out or &lt;a href="https://github.com/janpauldahlke/eris/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;.*&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
    </item>
    <item>
      <title>thinking to myself when setting up a project boilerplate</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Tue, 04 May 2021 17:33:39 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/what-iam-thing-to-myself-when-setting-up-a-project-boilerplate-1p94</link>
      <guid>https://dev.to/janpauldahlke/what-iam-thing-to-myself-when-setting-up-a-project-boilerplate-1p94</guid>
      <description>&lt;p&gt;this is a &lt;strong&gt;thinking out loud&lt;/strong&gt; to myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  how to prepare a SPA in different frameworks/libraries but also in angular
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ng new&lt;/code&gt; &lt;code&gt;create-react-app&lt;/code&gt; &lt;code&gt;vue cli&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;enable router&lt;/li&gt;
&lt;li&gt;enable i18n strategy&lt;/li&gt;
&lt;li&gt;enable store / reduxy stuff&lt;/li&gt;
&lt;li&gt;bind router into store&lt;/li&gt;
&lt;li&gt;ensure your API with smthng &lt;a href="https://marketplace.visualstudio.com/items?itemName=humao.rest-client"&gt;like&lt;/a&gt;  or &lt;code&gt;curls&lt;/code&gt; / postmany stugff &lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;test GET / POST/ UPDATE&lt;/li&gt;
&lt;li&gt;brief moment of think: do we need to auth? if so handle auth token with like setLocalStorage&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;create an entityfolder for selected endpoint, (if angular, also create a module here!!)&lt;/li&gt;
&lt;li&gt;create model/type declartion (only if typescript, which we prefer) of entities&lt;/li&gt;
&lt;li&gt;create service to consume any http-client (eg axios) // consider auth process
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;test GET / POST/ UPDATE &lt;code&gt;loadAll()&lt;/code&gt;, &lt;code&gt;oneOneById()&lt;/code&gt;, 
&lt;code&gt;postNew()&lt;/code&gt;, &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt; for selected entity&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;test servicemethods in bare minimum but routable component, most liekly in lifecycle method of given framework&lt;/li&gt;
&lt;li&gt;from now on we would need to distungiush from framework to framework/library which should be more articles, &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;in angular -which we atm work with- we go with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create routeresolvers&lt;/li&gt;
&lt;li&gt;prepare actions / reducer / selectors from ngrx, naivly bind http service into the loadaction (we like ngrx-entity here) &lt;/li&gt;
&lt;li&gt;access store in template and trigger action for udf to ensure actions work&lt;/li&gt;
&lt;li&gt;create effect for entity -&amp;gt; &lt;code&gt;loadAllEntity&lt;/code&gt; -&amp;gt; http-service -&amp;gt; dispatch &lt;code&gt;loadAllEntitySucess&lt;/code&gt;, bind result into store&lt;/li&gt;
&lt;li&gt;we forget about, register router, effect, store, store-router in module, considering it silently given from now on, knowing forRoot() and forFeature(). &lt;/li&gt;
&lt;li&gt;remove 3 from template&lt;/li&gt;
&lt;li&gt;consume loadEntity from action in routeresolver -&amp;gt; which should also will trigger the effect -&amp;gt; which will trigger the http -&amp;gt; which will trigger store binding&lt;/li&gt;
&lt;li&gt;bind observable/subscriber to store in template list view&lt;/li&gt;
&lt;li&gt;prepare route for single entity&lt;/li&gt;
&lt;li&gt;access route, write resolver that gains single entity from store / write actions..... from here on&lt;/li&gt;
&lt;li&gt;rinse repaet&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;quod erat demonstrandum.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>boilerplate</category>
    </item>
    <item>
      <title>looking for a good vpn provider</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Sat, 09 Jan 2021 16:14:05 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/looking-for-a-good-vpn-provider-2e53</link>
      <guid>https://dev.to/janpauldahlke/looking-for-a-good-vpn-provider-2e53</guid>
      <description>&lt;p&gt;I just asked this on the Typescript Discord Channel -- Offtopic, but tought why not enhance my question radius.&lt;/p&gt;

&lt;p&gt;[17:08] Paulquapper: could somone suggest my a VPN provider with &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. a documented (javascripty) API and &lt;/li&gt;
&lt;li&gt;2. the feature torenew ip, for given city/country. Ideally non US, no US-Server provider.&lt;/li&gt;
&lt;li&gt;3. "cost efficient" (paying annualy)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>question</category>
      <category>vpn</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Need help to improve my usage of gatsby-image</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Thu, 19 Nov 2020 10:26:11 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/need-help-to-improve-my-usage-of-gatsby-image-mac</link>
      <guid>https://dev.to/janpauldahlke/need-help-to-improve-my-usage-of-gatsby-image-mac</guid>
      <description>&lt;p&gt;Hello fellow Gatsby'ers. Today i am comming with a question to you. I need to improve my bundle size, and therefore i want to rewrite my gatsby-image wrapper to be more efficient. &lt;/p&gt;

&lt;p&gt;Consider my bundle&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Gd_fGVLt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sy22p5f5zxpobphoosxx.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Gd_fGVLt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sy22p5f5zxpobphoosxx.PNG" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BUT: the 150 images itself are only 14MB in total, which means gatsby bloats it like crazy.&lt;/p&gt;

&lt;p&gt;I know my code smell in terms of BIG-O, but, my poor excuse, i didn't knew another way than mapping over the result. Can you guys help me to improve here, considering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImageLarge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StaticQuery&lt;/span&gt;
      &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;graphql&lt;/span&gt;&lt;span class="s2"&gt;`
        query {
          allFile(filter: { sourceInstanceName: { eq: "assets" } }) {
            edges {
              node {
                relativePath
                name
                childImageSharp {
                  fluid(maxWidth: 600, quality: 100) {
                    ...GatsbyImageSharpFluid
                  }
                }
              }
            }
          }
        }
      `&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and here now my pile of shame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Btw, my problem are comming from not beeing able to parametrize my static gqlquery with arguments, so i took all images, mapped over and returned only the image that i needed. horrible inifficnient. &lt;/p&gt;

&lt;p&gt;Why cant i do something like this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImageLarge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StaticQuery&lt;/span&gt;
      &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;graphql&lt;/span&gt;&lt;span class="s2"&gt;`
        query {
          allFile(filter: { sourceInstanceName: { eq: `&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="s2"&gt;` } }) {
            edges {
              node {
                relativePath
                name
                childImageSharp {
                  fluid(maxWidth: 600, quality: 100) {
                    ...GatsbyImageSharpFluid
                  }
                }
              }
            }
          }
        }
      `&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I asked google and found this, which describes my problem perfectly, but offers no solution&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Believe it or not you cannot create a fully dynamic image component using a gastby-image without risking a (possibly very large) bloat in your bundle size. The problem is that static queries in Gatsby do not support string interpolation in it's template literal."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/57098239/what-is-the-best-way-to-display-all-pictures-of-a-directory-in-a-gatsby-project"&gt;what-is-the-best-way-to-display-all-pictures-of-a-directory-in-a-gatsby-project&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If one you could help me to solve this problem, i'd be glad and i also think, that we should write about it here, so others can find this googeling.&lt;/p&gt;

&lt;p&gt;May the code be with you 🙏&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>react</category>
      <category>graphql</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Javascript Homeoffice Stream</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Thu, 12 Nov 2020 09:46:46 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/javascript-homeoffice-stream-5ajk</link>
      <guid>https://dev.to/janpauldahlke/javascript-homeoffice-stream-5ajk</guid>
      <description>&lt;p&gt;Hey fellow nerds,&lt;/p&gt;

&lt;p&gt;out of boredom in the homeOffice era, i started my very "unique" frontend stream. Feel free to join and chit chat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.twitch.tv/paulquappe"&gt;https://www.twitch.tv/paulquappe&lt;/a&gt; :-)&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to deal with real imposters?</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Thu, 12 Nov 2020 09:43:26 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/how-to-deal-with-real-imposters-5d08</link>
      <guid>https://dev.to/janpauldahlke/how-to-deal-with-real-imposters-5d08</guid>
      <description>&lt;p&gt;Since we all read a lot about imposter syndrom, we tend to see an unrecognized genius in our collegues.&lt;/p&gt;

&lt;p&gt;BUT? How to deal with an actual real imposter in your job?&lt;/p&gt;

&lt;p&gt;I have been hired for a nice project, JAVA Backend + REACT (SSR) Frontend, Webassembly, webGL and Azure, just to bring you some buzzwords. The run is planed for like 5 years, so to say, it is today already planned, what they want to do with the product in the future.&lt;/p&gt;

&lt;p&gt;After 10 years of beeing a developer, mostly Frontend, but also JSP/jinja/smartie and other templating stuff, i decided to take this position for the following reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the stack looks familiar, but also there are some new things&lt;/li&gt;
&lt;li&gt;the lifetime of the project (to comparsion, most agency project last 3 months in coding)&lt;/li&gt;
&lt;li&gt;there will be a new team formed, since we take the project over and make it an internal company project&lt;/li&gt;
&lt;li&gt;my desire to become a senior developer (not only moneywise)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And when they hired me, they said, there will be an IT Team soon in our company. So far, so good.&lt;/p&gt;

&lt;p&gt;Then "the corana" happened and it was decided to hire only the Senior Architect, the HR found a promising candidate. No, they don't liked the indian guys, sadly. Still i  bet he was good at JAVA but bad in german (since iam german). &lt;br&gt;
They offered the job to someone else. It is a handsome, very charming, young and old, 10 years of experience as team-leader "senior software architect".&lt;/p&gt;

&lt;p&gt;Just after stalking him on xing (our german linkedin) i realized, that his buzzwords never fit to each other. he has 4 star experience in &lt;code&gt;C++/C#&lt;/code&gt;!! &lt;/p&gt;

&lt;p&gt;??&lt;br&gt;
!!&lt;/p&gt;

&lt;p&gt;WTF, in either one, or in both? i mean for me, its been worlds between microsoft .NET and C++, but ok...&lt;/p&gt;

&lt;p&gt;Then he has 4 Stars in JAVA/Android Studio? Hu? Java OR android or both or what? Iam confused.&lt;/p&gt;

&lt;p&gt;But, i am the smallest lightbulb in the world, so they hired him, and he gets paid well, since i know my salary, which is pretty ok, but it is not the money i am about.&lt;/p&gt;

&lt;p&gt;After talking to him for an hour, i realized he mixes terminology up. From my experience, if i dont develop daily, skills decay. I forget things, even the &lt;code&gt;any&lt;/code&gt; type in Hypescript (joke is on me).&lt;/p&gt;

&lt;p&gt;I wonder if it is reasonable to think, that someone who has not been in coding for 10 years, since his resumee is saying "PROJEKT MANAGER" and not "DEVELOPER", simply can not code.&lt;/p&gt;

&lt;p&gt;We talked. He didn't know that JAVA servers are configurable for the MEM they are allowed to use. &lt;br&gt;
He has no ****ing clue about JAVA changing it's business model and versioning, openjdk vs oracle java.&lt;/p&gt;

&lt;p&gt;He messes up PUT and POST and sends the same JSON-body on the same route, and asks me after two hours "why is this not working" (we use a swagger).&lt;/p&gt;

&lt;p&gt;So i told him: "please be honest, how good is your JAVA?"&lt;br&gt;
He replied: "i can learn fast, iam just learning spring boot on udemy right now."&lt;br&gt;
And then he said, and this made me really listen up. "In the job description the where looking for a nodejs guy", So his resume has 4 of 5 stars in JAVA/android, but he applied for nodejs?&lt;/p&gt;

&lt;p&gt;JEZ. I get confused about all the wrong buzzwords and project manager gibber he is talking. &lt;/p&gt;

&lt;p&gt;My boss asked me, what i think about my new collegue. What would you say in this moment? I tend to be diplomatic. "He is really nice. I like him. I hope he can show me things in backend, that will make me senior in 4-5 years too."&lt;/p&gt;

&lt;p&gt;I know this differs to the usual dev.to stuff, it's more a writeup. But let me get to the point.&lt;/p&gt;

&lt;p&gt;I think he is an imposter, since he knows what is paid in IT.&lt;/p&gt;

&lt;p&gt;How would you react if you find out your colleque is a liar about his skillset, wirite some tipps in the comments. &lt;/p&gt;

&lt;p&gt;TL:DR&lt;br&gt;
 Among all the smart guys with imposter-syndrome, are some real imposters, for the lulz and the $$$$&lt;/p&gt;

</description>
      <category>career</category>
    </item>
    <item>
      <title>console.fun() or how to mess up the DOM</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Wed, 13 Mar 2019 19:15:11 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/consolefun-or-how-to-mess-up-the-dom-2kkb</link>
      <guid>https://dev.to/janpauldahlke/consolefun-or-how-to-mess-up-the-dom-2kkb</guid>
      <description>&lt;p&gt;I stumbled upon this little gem in the &lt;a href="https://vue-land.js.org/"&gt;vue-land&lt;/a&gt; #random channel and it made me smile. Posted by &lt;em&gt;xan#2554&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Thought maybe it can make you smile too and decided to share it to you. Have fun.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instructions
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;copy script&lt;/li&gt;
&lt;li&gt;visit your most preferred website -- ..most likely &lt;a href="//dev.to"&gt;dev.to&lt;/a&gt; 😜&lt;/li&gt;
&lt;li&gt;open console&lt;/li&gt;
&lt;li&gt;paste script and press &lt;code&gt;enter&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funnyFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;screenwidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;screenheight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;randT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;screenheight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;randL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;screenwidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transition-duration:500ms; transition-property: left, top; position: fixed; top:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;randT&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px; left:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;randL&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;funnyFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&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;Be honest to yourself, did you knew what will happen? &lt;/p&gt;

&lt;p&gt;cheers&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>console</category>
      <category>fun</category>
    </item>
    <item>
      <title>i installed a 9 month old mini project again</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Fri, 09 Nov 2018 09:08:29 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/i-installed-a-9-month-old-mini-project-again-4nbm</link>
      <guid>https://dev.to/janpauldahlke/i-installed-a-9-month-old-mini-project-again-4nbm</guid>
      <description>&lt;p&gt;I installed a npm project again after 9 months...&lt;br&gt;
The package.json contained less then 10 entries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fPMIKX2z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/u19Rf6L.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fPMIKX2z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/u19Rf6L.png" alt="wtf"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This feels insecure, somehow.... but iam not an expert ;-)&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>npm</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to improve the build speed in React-Typescript, when using material ui</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Mon, 22 Oct 2018 09:44:47 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/how-to-improve-material-ui-speed-in-react-typescript-1199</link>
      <guid>https://dev.to/janpauldahlke/how-to-improve-material-ui-speed-in-react-typescript-1199</guid>
      <description>

&lt;p&gt;Hi,&lt;br&gt;
you are using material - ui in react typescript and having trouble with the performance? Well i stumpled upon the same issue.&lt;br&gt;
The build takes like forever, since i started to use material-ui and i was really no fun to code against such a lame project.&lt;/p&gt;

&lt;p&gt;But lets go in medias res and describe the scenario:&lt;/p&gt;

&lt;p&gt;I wrote a header component like this&lt;br&gt;
&lt;a href="https://i.imgur.com/3pMF0hO.png"&gt;Header&lt;/a&gt;. Pretty staright forward, right?&lt;/p&gt;

&lt;p&gt;While coding i went really unhappy, because the build and the livereolad grew pretty slow. So i played around with the Typescript compiler and found this little command&lt;br&gt;
&lt;code&gt;tsc --diagnostics --listFiles&lt;/code&gt; which i made an alias &lt;code&gt;healtcheck&lt;/code&gt; in my package.json. &lt;/p&gt;

&lt;p&gt;Running it was painfull and ended in results like&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Files:          5598
Lines:        108833
Nodes:        453997
Identifiers:  146625
Symbols:      217251
Types:         56743
Memory used: 300772K
I/O read:      4.66s
I/O write:     0.48s
Parse time:   26.90s
Bind time:     1.62s
Check time:    9.72s
Emit time:     1.64s
Total time:   39.87s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;As you guys can easily see, it takes quite some time and runs to a TON of files.&lt;/p&gt;

&lt;p&gt;I imported the two icons for the header like this:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
  AccountCircle,
  Language
} from '@material-ui/icons';
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And i was wondering about the mass of files that &lt;code&gt;--diagnostics&lt;/code&gt; considered when building, especially the trillions of &lt;code&gt;/node_modules/@material-ui/icons/**.d.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Yes iam a typescript user, and althought i had some troubles to get into it, i really like it now. As you might know, if a package does not bring its own typings, there is a huge community in &lt;a href="https://github.com/DefinitelyTyped/"&gt;DefinitlyTyped&lt;/a&gt; on github, that provides typings for large packages.&lt;/p&gt;

&lt;p&gt;For example: one would get the typings for material-ui like this: &lt;code&gt;npm install --save-dev @types/material-ui&lt;/code&gt;, with the result of beeing able to consume material-ui in the typescript world.&lt;/p&gt;

&lt;p&gt;But a build time of nearly 40seconds? I wanted to improve this and did some research at big brother.&lt;br&gt;
Found this github &lt;a href="https://github.com/mui-org/material-ui/issues/11916"&gt;issue&lt;/a&gt; that point to the same problem. And i was really happy to read this &lt;a href="https://github.com/mui-org/material-ui/issues/11916#issuecomment-429984950"&gt;comment&lt;/a&gt; with a major improvement.&lt;/p&gt;

&lt;p&gt;Instead of importing like above, i simply rewrote my import of the icons to:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import AccountCircle from '@material-ui/icons/AccountCircle';
import  Language from '@material-ui/icons/Language';
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Lets see how much this impacts our &lt;code&gt;healtcheck&lt;/code&gt; now:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Files:           382
Lines:         98396
Nodes:        386176
Identifiers:  130972
Symbols:      201601
Types:         56743
Memory used: 245687K
I/O read:      0.35s
I/O write:     0.37s
Parse time:    4.29s
Bind time:     0.79s
Check time:    5.16s
Emit time:     1.44s
Total time:   11.69s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Wow! Iam was speechless by such a little change, but with an impact like this. We gained nearly 30seconds by altering 2 lines, i mean it could be more lines, but by &lt;strong&gt;NOT IMPORTING VIA THE INDEX&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Iam glad i could share this to you, and it will maybe help the one or another. &lt;/p&gt;

&lt;p&gt;My name is JanPaul and iam working for &lt;a href="http://www.instant-data.com/"&gt;instant-data&lt;/a&gt;. We are small company, that provides internal tooling for large agencies and also data-exchange platforms.&lt;/p&gt;

&lt;p&gt;May the code be with you :pray:&lt;br&gt;
Thanks for the title image to &lt;a href="https://unsplash.com/@emilbruckner"&gt;Emil Bruckner&lt;/a&gt; @unsplash.&lt;/p&gt;


</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>materialui</category>
      <category>react</category>
    </item>
    <item>
      <title>reducks, a boilerplate</title>
      <dc:creator>jan paul</dc:creator>
      <pubDate>Tue, 11 Sep 2018 18:27:54 +0000</pubDate>
      <link>https://dev.to/janpauldahlke/rate-my-boilerplate-j8c</link>
      <guid>https://dev.to/janpauldahlke/rate-my-boilerplate-j8c</guid>
      <description>

&lt;p&gt;&lt;a href="https://unsplash.com/@jaseess"&gt;img by Jase Ess&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;Hello dear dev.to,&lt;/h1&gt;

&lt;p&gt;Iam working for &lt;a href="http://www.instant-data.com/"&gt;instant-data&lt;/a&gt;. a small company that provides solutions in the environment of a larger advertising agency. mostly internal toolings.&lt;/p&gt;

&lt;p&gt;After playing round a little with the create-react-app, i wanted to share my latest boilerplate with you and ask for your opinions, or improvements. You can visit me  &lt;a href="https://github.com/janpauldahlke/react-ts-boilerplate-2018/"&gt;here&lt;/a&gt; on github.&lt;br&gt;
In this i follow the ducks pattern, &lt;b&gt; in which you try to keep all your redux and store reducer stuff in one single file&lt;/b&gt;, but for sakes of DRY, i will present you the README.MD. I appreciate any feedback ;-)&lt;/p&gt;

&lt;p&gt;It is created via the CLI-Tool &lt;br&gt;
&lt;br&gt;
&lt;code&gt;create-react-app &amp;lt;your-appname&amp;gt; --scripts-version=react-scripts-ts&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 and customized to my needs, by running &lt;br&gt;
&lt;br&gt;
&lt;code&gt;npm eject&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;Prequisites&lt;/h3&gt;

&lt;p&gt;Installed node and npm Version 8 or higher.&lt;/p&gt;

&lt;h2&gt;Available Scripts&lt;/h2&gt;

&lt;p&gt;In the project directory, you can run:&lt;/p&gt;

&lt;h4&gt;&lt;code&gt;npm start&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;Open &lt;a href="http://localhost:3000"&gt;http://localhost:3000&lt;/a&gt; to view it in the browser.&lt;/p&gt;

&lt;h4&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;Builds the app for production to the &lt;code&gt;build&lt;/code&gt; folder.&lt;/p&gt;

&lt;h4&gt;
&lt;code&gt;npm run seed&lt;/code&gt; Authentication seed&lt;/h4&gt;

&lt;p&gt;To make the boilerplate work from scratch, and to simulate the flow of Auth, we put a JSON-Server.&lt;br&gt;
You should remove the complete seed folder, or better add it to your &lt;code&gt;.gitignore&lt;/code&gt;, when you implement your own workflow. To make use of it simply &lt;code&gt;npm run seed&lt;/code&gt;. You will get served at &lt;code&gt;json-server --port 4000 --watch -- seed/db.json&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;Folder Structure&lt;/h4&gt;

&lt;p&gt;After creation, your project should look like this:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app/
  README.md
  node_modules/
  package.json
  public/
    index.html
    favicon.ico
  seed/
    /* a helper fake backend for firsttimers */  
  src/
    index.tsx
    components/
      App.tsx
    containers/
      /* your connected containers here */
    ducks/
      /* your ducks here */
    services/
      /* put all your global helpers inside here */
    store/
      history.ts
      index.ts
      localStorage.ts
  types/
    /*your types go here */  
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h4&gt;Debugging and DevTools&lt;/h4&gt;

&lt;p&gt;I strongly suggest you to install and use theses BrowserExtensions.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd"&gt;redux-devtools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi"&gt;react-devtools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Material-UI and Decorators&lt;/h4&gt;

&lt;p&gt;The experimental feature for Decorators is activated in the &lt;code&gt;tsconfig.json&lt;/code&gt;. By this it is possible to use the withStyle decorator in combination with &lt;code&gt;compose&lt;/code&gt; of the &lt;code&gt;recompose package&lt;/code&gt;. &lt;br&gt;
Example usage is:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'recompose'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withStyles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@material-ui/core/styles'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withStyles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;Example&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IExampleProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IExampleState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To make use of themes in material-ui we need to wrap the content of App inside a&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MuiThemeProvider&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;createMuiTheme&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;//any content here&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MuiThemeProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;a href="https://material-ui.com/customization/themes/"&gt;read more about Material UI Themes&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Environment&lt;/h3&gt;

&lt;p&gt;There is the possibility to provide ENV variables. use the &lt;code&gt;.env&lt;/code&gt; file for this.&lt;br&gt;
Please note, they need to follow the naming convention of &lt;code&gt;REACT_APP_&lt;/code&gt; &lt;code&gt;EXAMPLE=FOOBAR&lt;/code&gt;.&lt;br&gt;
Inside the project one can consume them by &lt;code&gt;proccess.env.REACT_APP_EXAMPLE&lt;/code&gt;. &lt;br&gt;
At a new start its set to the seedings path, so one might want to change it there.&lt;/p&gt;

&lt;h3&gt;HTTP Requests&lt;/h3&gt;

&lt;p&gt;Please use axios for your requests. It supports Promises and also async/await.&lt;br&gt;
&lt;a href="https://github.com/axios/axios"&gt;https://github.com/axios/axios&lt;/a&gt;&lt;br&gt;
Also make sure the allow crossorigin-access on your server.&lt;/p&gt;

&lt;h3&gt;Ducks&lt;/h3&gt;

&lt;p&gt;We follow the 'Ducks'-Pattern to structure our application. Get insights on this specc here&lt;br&gt;
&lt;a href="https://github.com/erikras/ducks-modular-redux"&gt;https://github.com/erikras/ducks-modular-redux&lt;/a&gt; &lt;br&gt;
The pattern is to have all these inside one duck:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;action enum&lt;/li&gt;
&lt;li&gt;action types&lt;/li&gt;
&lt;li&gt;action creators&lt;/li&gt;
&lt;li&gt;reducer functions&lt;/li&gt;
&lt;li&gt;mainreducer&lt;/li&gt;
&lt;li&gt;thunk&lt;/li&gt;
&lt;li&gt;initialStore&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Message Component&lt;/h3&gt;

&lt;p&gt;There is a rudimental example prepared in this boilerplate, that shows usage and consumption of a high level message or notification-component. Import the NofictationDuck inside the Ducks that you want the NotificationStore to be catched.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NotificationDuck&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'~/src/ducks/notification'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;your&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* success */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationDuck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;throwNotificationWithError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'your text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'your title'&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;But you can also connect them directly to components. Please keep in mind to reset the NotificationStore, with the &lt;code&gt;RESET_NOTIFICATION_STORE&lt;/code&gt; action! &lt;/p&gt;

&lt;h3&gt;Store&lt;/h3&gt;

&lt;p&gt;Use the folder &lt;code&gt;src/store&lt;/code&gt; to configure the store of redux according to your needs. There is also the possibilty to subscribe parts of the store to your localStorage. To do so check the &lt;code&gt;index.ts&lt;/code&gt; and have a brief look at the store subscribe pattern.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AuthStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AuthStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;saveState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;AuthStore&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Please make use of the &lt;code&gt;throttle&lt;/code&gt; lodash-helper. &lt;code&gt;JSON.parse()/JSON.stringyfy&lt;/code&gt; are expensive in Javascript.&lt;/p&gt;

&lt;h2&gt;Services&lt;/h2&gt;

&lt;p&gt;Anything you want to consume repetitive belongs to the services. Another word could be globalHelpers.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apicontroller -&amp;gt; an axois instance the can be configured. import it into your ducks to make &lt;code&gt;http&lt;/code&gt;-verbs.&lt;/li&gt;
&lt;li&gt;authenticationService -&amp;gt; can be used in conjunction with apicontroller inside of ducks to verify auth-Status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;inside your duck - an example usage in thunk&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'../../services/apicontroller'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="kr"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;:()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthDuck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAuthAction&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

      &lt;span class="c1"&gt;//return the axois object with header, pass the state by `getState()` to auth and GET the route with `get('/yourRoute')`&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/Authentication'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthDuck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAuthSuccessAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="cm"&gt;/**
        });
    };
  }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There is also a whole &lt;code&gt;react-router&lt;/code&gt; v4 part, i will explain at 50 :heart: :-)&lt;/p&gt;


</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>react</category>
      <category>reducks</category>
    </item>
  </channel>
</rss>
