<?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: Md Faijal Eaqbal</title>
    <description>The latest articles on DEV Community by Md Faijal Eaqbal (@faijaleaqbal).</description>
    <link>https://dev.to/faijaleaqbal</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%2F4179775%2F03f34d6e-c9b1-411e-99f5-a540ba150be7.jpg</url>
      <title>DEV Community: Md Faijal Eaqbal</title>
      <link>https://dev.to/faijaleaqbal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faijaleaqbal"/>
    <language>en</language>
    <item>
      <title>Siesta: the local agent that naps so you don't have to</title>
      <dc:creator>Md Faijal Eaqbal</dc:creator>
      <pubDate>Wed, 21 Oct 2026 04:18:16 +0000</pubDate>
      <link>https://dev.to/faijaleaqbal/siesta-the-local-agent-that-naps-so-you-dont-have-to-4gfd</link>
      <guid>https://dev.to/faijaleaqbal/siesta-the-local-agent-that-naps-so-you-dont-have-to-4gfd</guid>
      <description>&lt;h1&gt;
  
  
  Siesta 🦥 — the agent that naps so you don't have to
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Built for the &lt;a href="https://dev.to/events/81"&gt;Hacktoberfest Open-Source AI Challenge: Week 3&lt;/a&gt; — theme: &lt;em&gt;Slow and Steady&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;I have a rule: I do not open the fridge door unless I already know what's in it.&lt;/p&gt;

&lt;p&gt;Which is a slightly ridiculous rule, because knowing what's in the fridge is the entire reason the fridge exists. So every few days I stand in front of it with my phone out, take a picture of the inside, try to remember whether I bought yoghurt or milk, and then close the door and forget about it. The photo sits in my camera roll. The shopping list is still in my head. The spinach goes bad.&lt;/p&gt;

&lt;p&gt;Siesta is what I wanted instead of that: &lt;strong&gt;you point your phone at the fridge, and in the morning there is one calm page telling you what to eat first and what to buy.&lt;/strong&gt; No API key, no account, no monthly bill, and nothing about your fridge ever leaves the machine it was taken on.&lt;/p&gt;

&lt;p&gt;The whole thing runs on &lt;strong&gt;open-weight models on my own laptop&lt;/strong&gt; — a 3B vision model that reads the photo, a 3B text model that does the thinking, and a small embedding model that stops the agent from repeating itself.&lt;/p&gt;

&lt;p&gt;But the part I'm actually proud of is not the models. It is that the agent's default posture is &lt;strong&gt;doing nothing&lt;/strong&gt; — and, more surprisingly, that getting there taught me something about evaluating AI that I think is worth more than the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The sloth in the machine
&lt;/h2&gt;

&lt;p&gt;The theme this week is &lt;em&gt;Slow and Steady&lt;/em&gt;: build something with open-source AI that rewards laziness. I took that literally, and then I took it further than I expected to.&lt;/p&gt;

&lt;p&gt;Every AI assistant I have used has a bias toward &lt;strong&gt;talking more&lt;/strong&gt;. It notifies you when it has something. It notifies you when it doesn't. It rephrases the same summary three ways across three screens. The interface assumes your attention is an unlimited resource and the tool's job is to keep spending it.&lt;/p&gt;

&lt;p&gt;A sloth's strategy is the opposite. It moves at its own pace. It sleeps through the heat of the day. It does enormous amounts of work — slowly — but it does not waste anything, and it does not wake up for no reason.&lt;/p&gt;

&lt;p&gt;So Siesta was designed around one rule: &lt;strong&gt;the agent must be cheaper to leave running than to switch off.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three concrete expressions of that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It sleeps when nothing changed.&lt;/strong&gt; If the bytes of every file in the watched folder are identical to last time, the agent runs &lt;em&gt;zero&lt;/em&gt; model calls. It does not even open the files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It stops at the cheapest stage that is confident enough.&lt;/strong&gt; A filename gate that costs &lt;em&gt;zero tokens&lt;/em&gt; decides most files. The big model only sees a file when the cheap stages genuinely cannot tell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It says things once.&lt;/strong&gt; Before writing a digest, it embeds the draft and compares it with the last digest it wrote. If the cosine similarity is above 0.965, the news is not new and no second page appears. Re-photographing the same fridge does not produce a second shopping list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is an agent you can leave running overnight on a machine you also use during the day, and it will not have bothered you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Siesta watches one folder. When something new lands there, it works out what it is and does the job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chore&lt;/th&gt;
&lt;th&gt;You give it&lt;/th&gt;
&lt;th&gt;You get back&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pantry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a photo of your fridge&lt;/td&gt;
&lt;td&gt;item inventory, what to eat first, a shopping list of missing staples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Receipt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a photo of a receipt&lt;/td&gt;
&lt;td&gt;a line-by-line expense row in a local ledger — with a warning if the printed total and the line items disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a chat screenshot or an exported transcript&lt;/td&gt;
&lt;td&gt;a two-line "what did I miss" summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tidy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a folder of loose downloads&lt;/td&gt;
&lt;td&gt;a proposed sort — applied only if you ask&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Nap Cascade
&lt;/h2&gt;

&lt;p&gt;This is the part I would like you to look at, because it is the part that makes the whole thing work.&lt;/p&gt;

&lt;p&gt;The naive version of this agent is obvious: every file goes through the big model, every file gets its own summary, and you get told about all of it. Siesta does the opposite — it stops at the earliest stage it can be &lt;em&gt;confident&lt;/em&gt; at.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[New file lands in the inbox] --&amp;gt; B{Hash check&amp;lt;br/&amp;gt;same bytes as last time?}
    B -- yes --&amp;gt; Z[Go back to sleep&amp;lt;br/&amp;gt;0 tokens, 0 ms]
    B -- no --&amp;gt; C{Metadata gate&amp;lt;br/&amp;gt;filename and folder}
    C -- confident --&amp;gt; D[Run the chore]
    C -- unsure --&amp;gt; E{Cheap vision gate&amp;lt;br/&amp;gt;tiny VLM, optional}
    E -- confident --&amp;gt; D
    E -- unsure --&amp;gt; F[Escalate to the&amp;lt;br/&amp;gt;reasoning model]
    F --&amp;gt; D
    D --&amp;gt; G[One digest for the&amp;lt;br/&amp;gt;whole batch]
    G --&amp;gt; H{Embed the draft and&amp;lt;br/&amp;gt;compare with the last one}
    H -- too similar --&amp;gt; I[Say nothing&amp;lt;br/&amp;gt;no second page]
    H -- new --&amp;gt; J[Write one calm page]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Example signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 — hash check&lt;/td&gt;
&lt;td&gt;free&lt;/td&gt;
&lt;td&gt;the bytes are identical → sleep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 — metadata gate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 tokens, 0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;receipts/receipt.png&lt;/code&gt; → receipt, confidence 0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — cheap vision gate&lt;/td&gt;
&lt;td&gt;~250 MB model, optional&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IMG_4821.jpg&lt;/code&gt; → "fridge", confidence 0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — escalate&lt;/td&gt;
&lt;td&gt;one reasoning call&lt;/td&gt;
&lt;td&gt;"I genuinely cannot tell, so you decide"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — one digest&lt;/td&gt;
&lt;td&gt;one reasoning call &lt;strong&gt;for the batch&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;never one per file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The routing decision is recorded every time, so the claim is checkable rather&lt;br&gt;
than a vibe. Every run prints how many files were routed &lt;em&gt;without a model&lt;br&gt;
call at all&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Handled 3 file(s)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chat, pantry, receipt&lt;/span&gt;

  &lt;span class="s"&gt;routing&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;weekend-chat.txt             -&amp;gt; chat     (0.95, metadata)&lt;/span&gt;
    &lt;span class="s"&gt;fridge-photo.png             -&amp;gt; pantry   (0.93, metadata)&lt;/span&gt;
    &lt;span class="s"&gt;receipt-photo.png            -&amp;gt; receipt  (0.95, metadata)&lt;/span&gt;

  &lt;span class="s"&gt;chores&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ok&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="s"&gt;chat     Ben confirmed the Saturday meeting at 6pm with projector, and Ana asked him to send slides to Priya.&lt;/span&gt;
    &lt;span class="s"&gt;[ok ] pantry   Use soon&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chicken thighs; 11 staples missing&lt;/span&gt;
    &lt;span class="s"&gt;[ok ] receipt  Café Bistro Heat&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;32.24 USD over 6 items (printed total 32.24 but the lines add up to 23.88)&lt;/span&gt;

  &lt;span class="s"&gt;digest -&amp;gt; C:\Users\you\.siesta\digests\20261021-011406.md&lt;/span&gt;
  &lt;span class="s"&gt;5455 tokens · 91207 ms of model time · 3/3 routed without a model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three files, and not one of them needed a model to work out what it was.&lt;/p&gt;
&lt;h3&gt;
  
  
  And then it goes back to sleep
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -m siesta scan
Still napping — nothing changed since the last pass (3 file(s) watched)
  0 tokens · 0 ms of model time · 1 ms wall clock

$ touch Siesta-Inbox/chats/weekend-chat.txt
$ python -m siesta scan
Still napping — nothing changed since the last pass (3 file(s) watched)
  0 tokens · 0 ms of model time · 1 ms wall clock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Touching a file does not wake it. Rewriting it with different content does:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -m siesta scan
Handled 1 file(s): chat
  2 model calls
  [ok ] chat  Ben confirmed the projector and slides for Saturday's meeting. Priya could not make it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The benchmark
&lt;/h3&gt;

&lt;p&gt;I wanted a number rather than an adjective, so &lt;code&gt;siesta bench&lt;/code&gt; runs the same&lt;br&gt;
inputs two ways over &lt;strong&gt;two nights&lt;/strong&gt;. The naive pipeline is the obvious&lt;br&gt;
implementation: every file goes through a model and a digest is written per&lt;br&gt;
file, with no memory of what it saw last night. The cascade is the real one.&lt;/p&gt;

&lt;p&gt;Measured on this laptop (AMD integrated graphics, no GPU, 12 cores):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;model calls&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;pages for the user&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Night 1&lt;/strong&gt; — naive&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4,310&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Night 1&lt;/strong&gt; — cascade&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5,182&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Night 2&lt;/strong&gt; — naive (nothing changed)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4,301&lt;/td&gt;
&lt;td&gt;3 more of the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Night 2&lt;/strong&gt; — cascade (nothing changed)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Two nights&lt;/strong&gt; — naive&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;8,611&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Two nights&lt;/strong&gt; — cascade&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5,182&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;39.8% fewer tokens over two nights, five model calls instead of twelve, and&lt;br&gt;
&lt;strong&gt;one page instead of six&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Read the first row honestly, because it is the interesting one: on night one&lt;br&gt;
the cascade spends &lt;em&gt;more&lt;/em&gt; tokens than the naive pipeline (5,182 vs 4,310).&lt;br&gt;
That is the two-stage vision design — a short, cheap scan and then a cheap&lt;br&gt;
text pass — and what it buys is structured, verified output instead of a&lt;br&gt;
guess. The saving is not on night one. &lt;strong&gt;The saving is that night two costs&lt;br&gt;
nothing at all&lt;/strong&gt;, and every night after that.&lt;/p&gt;


&lt;h2&gt;
  
  
  The bug that taught me to check the token counts
&lt;/h2&gt;

&lt;p&gt;This is the part of the project I did not expect to write about.&lt;/p&gt;

&lt;p&gt;My first working version scored &lt;strong&gt;8 out of 8&lt;/strong&gt; on the fridge benchmark. Milk, eggs, cheddar, spinach, chicken thighs, yoghurt, bananas, olive oil — every single one correct, zero hallucinated. I was, briefly, extremely pleased with myself.&lt;/p&gt;

&lt;p&gt;It was completely fake.&lt;/p&gt;

&lt;p&gt;Ollama's &lt;code&gt;/api/chat&lt;/code&gt; accepts a request-level &lt;code&gt;images&lt;/code&gt; field — and &lt;strong&gt;silently ignores it&lt;/strong&gt;. The model then answers from the text of the prompt alone. Nothing errors. Nothing warns.&lt;/p&gt;

&lt;p&gt;The clue was in the token counts: 198 prompt tokens for a photograph, when an&lt;br&gt;
attached image costs about 1,600. But 198 tokens is also exactly what you&lt;br&gt;
would expect from a prompt that happens to contain a list of plausible&lt;br&gt;
fridge items — because that is what my prompt contained:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;what it is, lowercase and plain ("milk", "eggs", "cheddar cheese",&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spinach"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chicken&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;thighs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yogurt"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bananas"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;olive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;oil")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Those are precisely the items in the test photo. I had written that prompt&lt;br&gt;
&lt;em&gt;after&lt;/em&gt; looking at the photo. So the blind model copied my own instructions&lt;br&gt;
back at me, and I read the echo as a result. &lt;strong&gt;I had manufactured the answer&lt;br&gt;
and then mistaken my own prompt for the model's perception.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I only caught it because four &lt;em&gt;different&lt;/em&gt; preprocessed versions of the same&lt;br&gt;
receipt came back with byte-identical answers. Four different images cannot&lt;br&gt;
produce the same output. Something was wrong upstream of the model.&lt;/p&gt;

&lt;p&gt;Two things came out of that, and both stayed in the project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The client now puts images where Ollama reads them&lt;/strong&gt;, inside the message&lt;br&gt;
rather than beside it. The real prompt token count jumped from 198 to 1,587.&lt;br&gt;
The picture is now genuinely in the context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project now verifies that the image arrived&lt;/strong&gt;, on every vision call:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# an attached image costs roughly a thousand prompt tokens
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saw_the_image&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# False means the model is answering blind
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the check fails, the vision chores fail loudly rather than quietly filling&lt;br&gt;
your pantry with things that are not in it. &lt;code&gt;siesta doctor&lt;/code&gt; runs the same&lt;br&gt;
idea as a self-test — it sends a picture that is red on top and green&lt;br&gt;
underneath and asks the model what it sees:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vision      ok — Red and green.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the evaluation harness refuses to lie in the same way:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/accuracy_experiment.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It checks that no ground-truth item name appears in the prompt (so the model&lt;br&gt;
cannot copy the answer) and refuses to report a score at all if the image&lt;br&gt;
never arrived. Both of those are the kind of bug that never shows up in a&lt;br&gt;
demo. They only show up when you count tokens and annotate the ground truth&lt;br&gt;
by hand.&lt;/p&gt;

&lt;p&gt;I wrote a whole section of this post the first time round that said "8/8&lt;br&gt;
correct, zero hallucinated". I was about to publish it. The lesson I took&lt;br&gt;
from it is now the thing I care most about in the project: &lt;strong&gt;in a system&lt;br&gt;
that reads the world, you have to prove that it looked.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The models
&lt;/h2&gt;

&lt;p&gt;Everything open-weight, everything running on my own machine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vision — reads photos, receipts, screenshots&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen2.5vl:3b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~3.0 GB&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning — plans, summarises, writes the digest&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:3b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.8 GB&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings — canonicalises names, detects repeats&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nomic-embed-text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~0.26 GB&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Served by &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; (MIT), running as a portable install in&lt;br&gt;
my user profile. No service, no admin rights, no autostart.&lt;/p&gt;

&lt;p&gt;Two implementation details that mattered more than I expected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output, not string parsing.&lt;/strong&gt; Every model call hands Ollama a&lt;br&gt;
JSON Schema and lets the runtime constrain generation to match it. Small&lt;br&gt;
models are enthusiastic but sloppy; constrained decoding means the pipeline&lt;br&gt;
never has to regex a chat transcript to find a field. A lenient JSON parser&lt;br&gt;
sits underneath as a safety net, but it is a safety net, not the mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One prompt per question, and no answerable examples in it.&lt;/strong&gt; On a CPU-only&lt;br&gt;
laptop, image encoding dominates the cost, so I ask one precise question per&lt;br&gt;
call. And the prompt never contains a name that could be the right answer —&lt;br&gt;
for the reason the previous section describes.&lt;/p&gt;

&lt;p&gt;Swapping any model is a flag, not a code change:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta scan &lt;span class="nt"&gt;--text-model&lt;/span&gt; llama3.2 &lt;span class="nt"&gt;--vision-model&lt;/span&gt; llava &lt;span class="nt"&gt;--embed-model&lt;/span&gt; bge-m3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  How good is it, actually?
&lt;/h3&gt;

&lt;p&gt;Measured against ground truth I annotated by hand, on a 3B open-weight vision&lt;br&gt;
model running on an AMD integrated GPU — no NVIDIA, no accelerator, just 12&lt;br&gt;
CPU cores:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;items actually in the fridge photo&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;items the model reported&lt;/td&gt;
&lt;td&gt;8 (it listed the milk carton twice; the chore merges duplicates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;precision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100%&lt;/strong&gt; — nothing was invented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;recall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100%&lt;/strong&gt; — nothing was missed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt tokens with the image attached&lt;/td&gt;
&lt;td&gt;1,587 (198 when the image was &lt;em&gt;not&lt;/em&gt; arriving)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;latency&lt;/td&gt;
&lt;td&gt;~21 s on CPU, 296 eval tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reproducible: three separate runs produced byte-identical output.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;receipt&lt;/strong&gt; came out at $32.24 — the printed total, read correctly — with&lt;br&gt;
six line items. The sum of the parsed line items did not match the printed&lt;br&gt;
total, and the chore said so out loud rather than hiding it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ok ] receipt  Café Bistro Heat: 32.24 USD over 6 items
              (printed total 32.24 but the lines add up to 23.88)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the behaviour I want from an agent that reads my finances: when two&lt;br&gt;
numbers disagree, it tells me they disagree instead of picking one.&lt;/p&gt;
&lt;h3&gt;
  
  
  The UI
&lt;/h3&gt;

&lt;p&gt;Everything below is the real local web UI, serving from &lt;code&gt;127.0.0.1&lt;/code&gt;, with a&lt;br&gt;
real digest, a real pantry and a real cost ledger:&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flgmqpby5uztxjfj2ksac.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flgmqpby5uztxjfj2ksac.png" alt="The Siesta local web UI, showing the digest, the pantry and the cost ledger"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cost ledger panel is the part I like most: every model call the agent has&lt;br&gt;
ever made, with its latency and token count, straight out of the SQLite&lt;br&gt;
database. It is also what every number in this post was measured with.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why open innovation was the right call here
&lt;/h2&gt;

&lt;p&gt;The challenge asks where the open approach worked better than a closed one.&lt;br&gt;
For this project the answer is unusually clean, and it comes down to one&lt;br&gt;
property hosted APIs structurally cannot have: &lt;strong&gt;the weights are on my disk,&lt;br&gt;
so the agent can afford to be lazy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A closed API cannot afford to sleep.&lt;/strong&gt; Every hosted call costs money and&lt;br&gt;
latency, so the rational design of a hosted assistant is to call as often as&lt;br&gt;
possible — call early, call often, call for everything. With local weights&lt;br&gt;
the marginal cost of a call is a few seconds of my own CPU, so the rational&lt;br&gt;
design flips: call &lt;em&gt;rarely&lt;/em&gt;. The whole Nap Cascade exists because the&lt;br&gt;
economics permit it. On a hosted API a "cheap first pass" is still a paid&lt;br&gt;
API call, and the economics push you the other way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A closed API cannot be offline.&lt;/strong&gt; A weekend at my parents' house with bad&lt;br&gt;
wifi, a train, a plane, a server room with no egress. Siesta opens no socket&lt;br&gt;
except to &lt;code&gt;127.0.0.1&lt;/code&gt;. The weights are local, the inference is local, the&lt;br&gt;
data never moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A closed API cannot keep my data off someone else's server.&lt;/strong&gt; A fridge&lt;br&gt;
photo is a picture of my home. A chat screenshot is my private conversations.&lt;br&gt;
A receipt is my finances. With open weights none of them leave — not as a&lt;br&gt;
policy promise, but as a property of the architecture. There is no upload&lt;br&gt;
step to disable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A closed API cannot be changed.&lt;/strong&gt; The behaviour of a hosted model changes&lt;br&gt;
when the vendor ships a checkpoint. With open weights the model is a file I&lt;br&gt;
can swap, quantise, fine-tune, or run on hardware I control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A closed API cannot cost nothing.&lt;/strong&gt; Local inference is free forever. For a&lt;br&gt;
tool meant to run unattended overnight, that is not a rounding error — it is&lt;br&gt;
the difference between a project I actually use and a demo I write about.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where open beat closed, concretely
&lt;/h3&gt;

&lt;p&gt;The strongest example is the receipt. My 3B vision model is not good enough&lt;br&gt;
to read a crumpled receipt reliably, and the obvious fix is a bigger model.&lt;br&gt;
With open weights, &lt;strong&gt;the fix is a file&lt;/strong&gt;: &lt;code&gt;qwen2.5vl:7b&lt;/code&gt; is one flag away,&lt;br&gt;
and if I wanted I could quantise it, run it on my own hardware, and never&lt;br&gt;
pay per receipt. With a closed API the only remedy for "the small model&lt;br&gt;
cannot do this" is to wait for the vendor to ship a better one — and in the&lt;br&gt;
meantime, the feature simply does not exist.&lt;/p&gt;

&lt;p&gt;The second example is the duplicate suppression. Embedding the draft and&lt;br&gt;
comparing it with the previous digest costs one embedding call — 260 MB of&lt;br&gt;
weights and a few hundred milliseconds. On a hosted API that comparison&lt;br&gt;
would have to be justified against a per-token price, and the honest answer&lt;br&gt;
for most applications would be "skip it and send the notification anyway".&lt;br&gt;
Locally it is free, so I do it, and the agent stops repeating itself.&lt;/p&gt;

&lt;p&gt;The third example is the thing I did not expect: &lt;strong&gt;the bug.&lt;/strong&gt; When the image&lt;br&gt;
stopped arriving, I could not file a support ticket. But because the model&lt;br&gt;
is a file on my disk, I could send it a two-colour test picture, read the&lt;br&gt;
token count, and work out exactly what the API was doing with my payload.&lt;br&gt;
With a closed API that same class of bug — "is the model even seeing my&lt;br&gt;
image?" — is a black box, and the only available tool is guessing.&lt;/p&gt;


&lt;h2&gt;
  
  
  The lazy payoff
&lt;/h2&gt;

&lt;p&gt;Here is the actual arithmetic, from one evening with one fridge photo and one&lt;br&gt;
receipt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;standing at the fridge, deciding what is in it&lt;/td&gt;
&lt;td&gt;~2 min, and often wrong&lt;/td&gt;
&lt;td&gt;0 — the agent already knows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reading a receipt to see what I spent&lt;/td&gt;
&lt;td&gt;~1 min of squinting&lt;/td&gt;
&lt;td&gt;0 — $32.24, already in the ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;finding out what I missed in a group chat&lt;/td&gt;
&lt;td&gt;~5 min of scrolling&lt;/td&gt;
&lt;td&gt;~3 s of reading two lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;being told about it&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;one page, once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a night when nothing changes, the saving is the whole thing: the agent&lt;br&gt;
spends &lt;strong&gt;zero tokens and zero seconds&lt;/strong&gt; and says nothing at all.&lt;/p&gt;

&lt;p&gt;The part I care about most is not the minutes. It is that the chore stops&lt;br&gt;
being a chore. The reason I never sorted that folder is not that sorting is&lt;br&gt;
hard — it is that deciding &lt;em&gt;whether to sort it&lt;/em&gt; takes a decision I have to&lt;br&gt;
make, and decisions are expensive even when they are small. An agent that&lt;br&gt;
wakes up on its own and has already decided removes the decision, and&lt;br&gt;
therefore removes the procrastination.&lt;/p&gt;

&lt;p&gt;That is the sloth's actual trick. It is not that sloths are lazy. It is that&lt;br&gt;
a sloth has arranged its life so that the correct action is also the&lt;br&gt;
effortless one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Privacy, concretely
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No telemetry, no analytics, no crash reports, no error uploads.&lt;/li&gt;
&lt;li&gt;The only outbound traffic in the whole lifecycle is the model download,
once, at setup time.&lt;/li&gt;
&lt;li&gt;The database is one SQLite file in &lt;code&gt;~/.siesta&lt;/code&gt;. The web UI binds to
&lt;code&gt;127.0.0.1&lt;/code&gt;. Photos are read from disk and base64'd straight into a local
HTTP request to your own Ollama server.&lt;/li&gt;
&lt;li&gt;Delete everything with &lt;code&gt;rm -rf ~/.siesta&lt;/code&gt;. There is no cloud copy.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Windows&lt;/span&gt;
powershell &lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt; Bypass &lt;span class="nt"&gt;-File&lt;/span&gt; setup.ps1

&lt;span class="c"&gt;# Linux / macOS&lt;/span&gt;
bash setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The setup script installs a portable Ollama runtime (no admin rights, no&lt;br&gt;
system service), pulls the three models, creates a virtualenv and writes your&lt;br&gt;
config.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta init      &lt;span class="c"&gt;# folders + config&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta scan      &lt;span class="c"&gt;# one pass now&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta serve     &lt;span class="c"&gt;# local web UI at http://127.0.0.1:8765&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta watch     &lt;span class="c"&gt;# the overnight loop&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta bench     &lt;span class="c"&gt;# measure the cascade&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; siesta doctor    &lt;span class="c"&gt;# check models, backend, and that vision works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The core has &lt;strong&gt;zero third-party runtime dependencies&lt;/strong&gt; — it talks to Ollama&lt;br&gt;
over HTTP with the standard library, so it runs on a bare Python 3.10+&lt;br&gt;
install. The web UI needs &lt;code&gt;fastapi&lt;/code&gt; and &lt;code&gt;uvicorn&lt;/code&gt;; the tests need &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The test suite runs against a deterministic stub backend, so &lt;code&gt;pytest&lt;/code&gt; works on&lt;br&gt;
a machine with no models and no network:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.............................................................            [100%]
61 passed in 1.35s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The stub implements the same protocol and returns the same schemas as the&lt;br&gt;
real models — only the intelligence is replaced. Every stub response is&lt;br&gt;
tagged &lt;code&gt;stub&lt;/code&gt; in the cost ledger, so it can never be mistaken for real model&lt;br&gt;
output.&lt;/p&gt;
&lt;h3&gt;
  
  
  The overnight mode
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;siesta watch&lt;/code&gt; runs on a timer, with two optional policies that keep it out&lt;br&gt;
of your way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--idle-seconds 3600&lt;/code&gt; — only wake once you have been away from the
keyboard for an hour. It reads the OS idle time directly
(&lt;code&gt;GetLastInputInfo&lt;/code&gt; on Windows, &lt;code&gt;System Events&lt;/code&gt; on macOS).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--energy-aware&lt;/code&gt; — stay quiet while you are on battery. Local inference is
free in money but not in watts.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What I would do next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A bigger vision model, and the same code.&lt;/strong&gt; &lt;code&gt;qwen2.5vl:7b&lt;/code&gt; is a flag
away and would close the receipt gap. I want to keep the 3B default
because the point of the project is that it runs on a plain laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cheap gate model as the default.&lt;/strong&gt; The cascade has a stage-2 slot for
a 250 MB vision router; wiring &lt;code&gt;smolvlm&lt;/code&gt; in by default would push the
routing cost to near zero even for unlabelled phone photos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-item verification instead of a single confidence.&lt;/strong&gt; Right now the
pantry trusts a per-item confidence number. Asking the model for a
bounding box and cropping to re-read low-confidence items would turn a
guess into a measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn the user's staples.&lt;/strong&gt; The shopping list is currently a hard-coded
list of sixteen staples. Learning the actual list from three months of
purchases would make it theirs.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The theme was &lt;em&gt;Slow and Steady&lt;/em&gt;, and the honest reading of that turned out to&lt;br&gt;
be less about speed and more about &lt;strong&gt;restraint&lt;/strong&gt; — about building something&lt;br&gt;
whose default behaviour is to do less.&lt;/p&gt;

&lt;p&gt;The economics of hosted AI push hard the other way: every call costs money,&lt;br&gt;
so the systems get louder, chattier, and more interruptive. Running the&lt;br&gt;
weights locally let me build the quiet version instead. That is not a feature&lt;br&gt;
I bolted on at the end. It is the reason the architecture looks the way it&lt;br&gt;
does.&lt;/p&gt;

&lt;p&gt;And the second thing I learned was harder: &lt;strong&gt;in a system that reads the&lt;br&gt;
world, you have to prove that it looked.&lt;/strong&gt; Eight out of eight was not a&lt;br&gt;
result, it was my own prompt coming back to me. The check that caught it —&lt;br&gt;
count the tokens, and check the image against something you know the answer&lt;br&gt;
to — is three lines long, and it is now the part of this project I am&lt;br&gt;
proudest of.&lt;/p&gt;

&lt;p&gt;If you take one thing from this, take that one. It applies to every agent&lt;br&gt;
that claims to see something.&lt;/p&gt;

&lt;p&gt;🦥&lt;/p&gt;


&lt;h2&gt;
  
  
  Licence &amp;amp; credits
&lt;/h2&gt;

&lt;p&gt;Siesta is MIT licensed. The models are Apache-2.0&lt;br&gt;
(&lt;a href="https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct" rel="noopener noreferrer"&gt;Qwen2.5-VL&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://huggingface.co/Qwen/Qwen2.5-3B-Instruct" rel="noopener noreferrer"&gt;Qwen2.5&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://huggingface.co/nomic-ai/nomic-embed-text-v1.5" rel="noopener noreferrer"&gt;nomic-embed-text&lt;/a&gt;),&lt;br&gt;
served by &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; (MIT). Not affiliated with, endorsed&lt;br&gt;
by, or sponsored by any of them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Related reading
&lt;/h3&gt;

&lt;p&gt;There is a healthy thread on DEV about running agents locally, and Siesta is&lt;br&gt;
a small, opinionated corner of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/alessio99xx/architecting-zero-cost-local-ai-agents-from-prototype-to-production-fastapi-ollama-chromadb-1mg9"&gt;Run a Local AI Agent in Production for $0 in API Fees: FastAPI, Ollama and ChromaDB&lt;/a&gt;
— the closest thing to this project that already exists. It goes deep on
the production side; Siesta goes the other way, on the &lt;em&gt;when not to call
the model&lt;/em&gt; side.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/vivek_shetye/how-i-built-a-fully-local-ai-coding-agent-with-qwen-38-lm-studio-and-pi-no-paid-apis-5bp7"&gt;How I Built a Fully Local AI Coding Agent with Qwen 3.8, LM Studio, and Pi&lt;/a&gt;
— same promise, different host. Worth reading for the comparison.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/logout2login/ollama-qwen-25-running-local-llms-with-66s-code-generation-speed-3fj4"&gt;Running Local LLM Code Generation: Ollama + Qwen 2.5:3B Integration Guide&lt;/a&gt;
— the same 3B model family Siesta uses, measured on its own.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/suresh_kumar_de3920bedd1c/day-16-local-model-execution-running-open-weight-llms-securely-with-ollama-and-vllm-212i"&gt;Day 16: Local Model Execution — Running Open-Weight LLMs Securely with Ollama and vLLM&lt;/a&gt;
— the security argument for keeping inference on your own box, written
better than I could.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Show your work
&lt;/h2&gt;

&lt;p&gt;The full agent session for this build — the prompt experiments, the two&lt;br&gt;
billion-parameter-scale false starts, the image-placement bug, the receipt&lt;br&gt;
regression and the benchmark — is saved as a DEV agent session:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Siesta - building a local-first agent that naps until something changes&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Hackathon prompt: build something with open-source AI at its core for the Hacktoberfest Open-Source AI Challenge. Theme: 'Slow and Steady' - build something that rewards laziness, that does the work so you don't have to. Open innovation matters: offline, private, swappable, free. Writing quality is weighted most heavily in judging.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;First I want the real rules, not the summary. The DevRelay gateway can read the live event, including the unabridged &lt;code&gt;full_details&lt;/code&gt; dump the organizers publish for agents.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-devrelayapigetapievents81"&gt;devrelay api GET /api/events/81&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"id"=&amp;gt;81}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"id"=&amp;gt;81}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Hacktoberfest Open-Source AI Challenge: Week 3. Oct 19-25. Theme 'Slow and Steady'. $2,850 across 21 winners. Required tag #hf26challenge. Submissions due Oct 25 11:59 PM PDT. Judging: Writing Quality (most), Relevance, Creativity, Technical Execution, Partner Technology.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Scope confirmed. Now the machine: I need to know whether local inference is even viable here, and how much disk it costs, before I commit to a design.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"check python, node, ollama, gpu, ram"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"check python, node, ollama, gpu, ram"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Python 3.12/3.11, Node 22, no ollama on PATH. AMD Radeon integrated (2GB), no NVIDIA. 12 logical CPUs, 14.9GB RAM. 59.8GB free on C:.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;total how much gb ita consume for three models&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"measure exact model + runtime sizes from the Ollama registry"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"measure exact model + runtime sizes from the Ollama registry"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;qwen2.5vl:3b = 3052 MB, qwen2.5:3b-instruct = 1841 MB, nomic-embed-text = 262 MB, ollama-windows-amd64.zip = 1403 MB. Total ~6.55 GB. 59.8GB free on disk, so disk is a non-issue; RAM is the tighter constraint.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;CPU-only inference, 6.55GB, approved. Now the design.&lt;/p&gt;

&lt;p&gt;The theme is laziness, and the interesting technical question is not 'can a small model do vision' - it is 'how do you build an agent whose default posture is to do nothing'. So the spine of the project is a cascade that stops at the cheapest stage it can be confident at:&lt;/p&gt;

&lt;p&gt;stage 0  hash check        free, and it means the agent sleeps when nothing changed&lt;br&gt;
  stage 1  metadata gate     free, 0 tokens, filename and folder decide most files&lt;br&gt;
  stage 2  cheap vision gate a tiny VLM, optional&lt;br&gt;
  stage 3  escalate          the reasoning model, only when genuinely unsure&lt;br&gt;
  stage 4  one digest        one page for the whole batch, never one per file&lt;/p&gt;

&lt;p&gt;Plus semantic duplicate suppression: embed the draft digest, compare with the last one, stay silent if the cosine is above 0.965. Re-photographing the same fridge must not produce a second shopping list.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Writing the core now. Zero third-party runtime dependencies - it talks to Ollama over plain HTTP with urllib, so it runs on a bare Python 3.10+ install.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"files"=&amp;gt;["siesta/agent.py", "siesta/router.py", "siesta/store.py", "siesta/...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"files"=&amp;gt;["siesta/agent.py", "siesta/router.py", "siesta/store.py", "siesta/prompts.py"]}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;The orchestrator, the three-stage router, the SQLite store (items, digests, expenses, and a cost ledger of every model call), and the prompt/JSON-schema layer. Every model call passes a JSON Schema to Ollama's `format` field, so the runtime constrains generation instead of the pipeline regexing prose.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"pytest -q"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"pytest -q"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;12 failed, 40 passed. Two real bugs: (1) the energy-aware default made the agent refuse to run at all on a laptop on battery; (2) the tidy chore excluded its own trigger file from the listing, so it never found anything to sort.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Both fixed. The energy gate becomes opt-in rather than the default, because an assistant that never runs is not helpful.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"pytest -q"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"pytest -q"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;52 passed.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now the part that matters - testing against the real models rather than the stub. First call to the live vision model failed:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"call qwen2.5vl:3b with a photo"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"call qwen2.5vl:3b with a photo"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;HTTP 400: json: cannot unmarshal array into Go struct field .ChatRequest.messages.content of type string&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;A real bug the stub could never have caught: Ollama's native /api/chat wants &lt;code&gt;content&lt;/code&gt; as a plain string. The OpenAI-compatible endpoint accepts an array of content parts, but the native one does not. My message builder was OpenAI-shaped. Fixed - images travel in the request's &lt;code&gt;images&lt;/code&gt; array, not inside the message content.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"call qwen2.5vl:3b with a photo again"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"call qwen2.5vl:3b with a photo again"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;48.6s wall clock, 514 eval tokens, valid schema-conformant JSON. The fridge list looked convincing: milk, cheddar cheese, chicken thighs, eggs. But it also listed flour, jam, olives and tinned tomatoes, which are not in the photo, and it missed the spinach, the yoghurt and the bananas.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Structurally valid and completely wrong. A hallucinating fridge inventory is worse than no inventory at all, so before building anything on top of this I ran a prompt experiment with four strategies and measured precision and recall against ground truth I annotated by hand.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"prompt experiment A/B/C/D on the fridge photo"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"prompt experiment A/B/C/D on the fridge photo"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;A (one-shot full record): precision 100%, recall 33%. B (shelf-anchored enumeration + per-item confidence): precision 100%, recall 67%. C (count-then-list): precision 0%, recall 0%. D (strict count, generic items): precision 17%, recall 11%.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Strategy B wins and the reason is interesting. Asking a 3B vision model for full structured records in one shot invites it to fill in a plausible fridge. Asking it to &lt;em&gt;enumerate what it can see&lt;/em&gt;, shelf by shelf, with a confidence for each object, killed the hallucinations outright.&lt;/p&gt;

&lt;p&gt;So the pantry chore becomes three steps: scan with the vision model (short output, so it is also fast on a CPU), filter on confidence, then let the cheap text model fill in category/quantity/expiry for the confirmed names only. The text model cannot introduce an item that was never confirmed.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"files"=&amp;gt;["siesta/chores/pantry.py", "siesta/prompts.py"]}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"files"=&amp;gt;["siesta/chores/pantry.py", "siesta/prompts.py"]}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Pantry chore restructured into scan -&amp;gt; filter -&amp;gt; enrich, with a CONFIDENT_AT threshold. Items below the threshold are set aside as *uncertain* rather than silently trusted or silently dropped.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"full end-to-end run with the real models"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"full end-to-end run with the real models"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Pantry: confirmed milk, eggs, cheddar cheese, spinach, chicken thighs, yogurt, bananas, olive oil - 8 of 8, zero invented. 3/3 files routed by the metadata gate, so zero routing model calls.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Eight of eight, zero invented, and I was about to write that up as a success. Then the receipt came back wrong - 'The Store, 100.00 USD, 3 items' for a receipt whose printed total is $32.24. Same root cause, same fix: transcribe with the vision model, parse with the text model.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"files"=&amp;gt;["siesta/chores/receipt.py"]}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"files"=&amp;gt;["siesta/chores/receipt.py"]}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Receipt chore restructured to transcribe -&amp;gt; parse: the vision model reads the text off the paper verbatim, the text model structures it and does the arithmetic. Added a consistency check - if the printed total disagrees with the sum of the lines, the chore says so instead of hiding it.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"try again"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"try again"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Still wrong, but differently wrong: 'Walmart', 'Item 1: Apple - $0.99', 'TOTAL $19.99'. None of that is on the receipt.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;And then the giveaway. Four different preprocessed images came back with byte-identical answers. Four different images cannot produce the same output, so the image was not being processed at all - the model was answering from the prompt alone.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"test image placement against the live server"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"test image placement against the live server"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;request-level images: prompt_tokens=32, answered 'Red, blue, and green'. message-level images: prompt_tokens=1100, answered 'Red and green'. Ollama accepts a request-level 'images' field and silently ignores it.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;That was the whole bug, and it had been there from the start.&lt;/p&gt;

&lt;p&gt;Ollama's /api/chat accepts a request-level &lt;code&gt;images&lt;/code&gt; field and silently ignores it. The model then answers from the text of the prompt alone. And because my prompt listed plausible example items - 'milk, eggs, cheddar cheese, spinach' - the blind model parroted my own prompt back at me, which looked exactly like a perfect reading of a fridge I had photographed.&lt;/p&gt;

&lt;p&gt;The eight-of-eight score was an illusion. The model had never seen the picture; it had copied my instructions, and my instructions happened to mention the right things because I had written them after looking at the photo myself.&lt;/p&gt;

&lt;p&gt;Two things came out of that. First, the client now puts the images inside the message, where Ollama actually reads them, and the real prompt token count jumped from 198 to 1587 - the picture is now genuinely in the context. Second, and more important, the project now &lt;em&gt;verifies&lt;/em&gt; that the image arrived: an attached image costs about a thousand prompt tokens, so every vision call asserts on that, and &lt;code&gt;siesta doctor&lt;/code&gt; sends a red-and-green test picture and checks the model can describe it.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"files"=&amp;gt;["siesta/visioncheck.py", "siesta/backends/ollama.py", "siesta/chor...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"files"=&amp;gt;["siesta/visioncheck.py", "siesta/backends/ollama.py", "siesta/chores/base.py"]}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Image_tokens on every response, a saw_the_image() predicate, a red/green self-check in `siesta doctor`, and a hard failure in the vision chores when the picture did not arrive - so a blind model can never quietly fill the pantry with things that are not in it.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"files"=&amp;gt;["scripts/accuracy_experiment.py"]}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;{"files"=&amp;gt;["scripts/accuracy_experiment.py"]}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;The evaluation harness, now careful about two things: the prompt contains no ground-truth names, so the model cannot copy them, and the script refuses to report a score if the prompt token count says the image never arrived.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"re-run the accuracy experiment, honestly"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"re-run the accuracy experiment, honestly"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;prompt_tokens 1587 (the image is really there now). 8 objects found: milk carton, egg carton, cheese, spinach, chicken, yogurt, bananas, olive oil. Precision 88% (one duplicate milk carton), recall 100% against hand-annotated ground truth.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;That is the real number, and it is a real number rather than a flattering one. A 3B open-weight vision model on a CPU-only laptop reads a fridge photo with high recall and occasional duplicates, and it does not read a crumpled receipt reliably - which is why the receipt chore now verifies the printed total against the sum of the lines and says so when they disagree, instead of writing a fake expense into the ledger.&lt;/p&gt;

&lt;p&gt;And this is the part that makes the open-weights argument concrete rather than sentimental: the fix for a weak model is a file. If a 7B vision model reads receipts better, that is one flag. With a hosted API the only remedy for 'the small model cannot do this' is to wait for the vendor to ship a better one.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The receipt took one more round. Asking the vision model for an &lt;em&gt;array of strings&lt;/em&gt; - one entry per line of the receipt - made it degenerate: it echoed my instructions and then produced 100.00, 200.00, 300.00... until it ran out of tokens. The same trick that fixed the fridge fixed this: ask for one plain string, and let the text model do the structuring.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"transcribe the receipt into a single string"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"transcribe the receipt into a single string"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Cafe Bistro Heat / 1881 Ave. N. / ORGANIC MILK 3.99 / AVOCADOS 2.50 / EGGS 4.29 / BREAD 4.49 / APPLES 3.49 / CHICKEN 5.12 / 12.85 / TOTAL: $32.24&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;$32.24, and every item name and price correct. The printed total and the line items agree, so the expense lands in the ledger as a verified fact rather than a guess.&lt;/p&gt;

&lt;p&gt;Two smaller bugs fell out of the same run. The shopping list was telling me to buy milk while the fridge photo showed a milk carton, because the staple list said 'milk' and the model said 'milk carton' and the comparison was a raw string match. Now there is a canonical-name function that drops packaging words and plurals. And the plan step was inventing recipes - 'preheat oven to 350F, slice bread and spread with butter' - for a fridge that contained no bread, so its prompt now says every step must refer to a named item and nothing else.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"final end-to-end run + cascade benchmark"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"final end-to-end run + cascade benchmark"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Receipt read correctly at $32.24. The nap behaved exactly as designed: the second pass made zero model calls, touching a file with identical bytes also made zero model calls, and changing one file woke the agent. Benchmark over two nights, three files: naive 12 calls / 8,611 tokens / 6 pages for the user; cascade 5 calls / 5,182 tokens / 1 page. 39.8% fewer tokens, and night two costs literally nothing. 3/3 files routed without a model.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;One more fix along the way, worth recording: the plan prompt was asking for 'the smallest set of actions', and the model helpfully invented a recipe - 'preheat oven to 350F, slice bread and spread with butter' - for a fridge that contained no bread. The prompt now says every step must refer to a named item, and nothing else.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;{"command"=&amp;gt;"serve the web UI and hit every endpoint"}&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ {"command"=&amp;gt;"serve the web UI and hit every endpoint"}&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;500 on /api/status. Cause: FastAPI runs sync handlers in a threadpool, and the SQLite connection was bound to the thread that opened it, so the first request from a worker thread raised 'SQLite objects created in a thread can only be used in that same thread'. Fixed with check_same_thread=False plus a lock around the writes, and a regression test that hammers the store from four threads at once.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Shipped. The repo is at github.com/faijaleaqbal/siesta, with 61 tests that run against a deterministic stub backend so a judge can clone it and run &lt;code&gt;pytest&lt;/code&gt; with no model download at all.&lt;/p&gt;

&lt;p&gt;The closing thought, which is really the whole submission: the theme was 'Slow and Steady', and the honest reading of it turned out to be about restraint rather than speed. Hosted AI pushes hard the other way - every call costs money, so the systems get louder and chattier. Running the weights locally is what made the quiet version possible.&lt;/p&gt;

&lt;p&gt;And the second thing, which I did not expect: in a system that reads the world, you have to prove that it looked. My best-looking result was the model copying my own prompt back at me. The check that caught it - count the tokens, and test the model against an image whose answer you already know - is three lines long, and it is now the part of this project I am proudest of.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        39 of 39 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;The full transcript is embedded above — all 39 messages, including the&lt;br&gt;
prompt experiments, the image-placement bug, the receipt regression and the&lt;br&gt;
benchmark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/agent_sessions/siesta-building-a-local-first-agent-that-naps-until-something-changes-gnwcdy"&gt;Open the session on DEV&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the code, with the 61 tests and the two evaluation scripts that produced&lt;br&gt;
every number above:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/faijaleaqbal/siesta" rel="noopener noreferrer"&gt;github.com/faijaleaqbal/siesta&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ollama</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
