How I built a local AI assistant that admits when it didn't actually do the thing
How I built a local AI assistant that admits when it didn't actually do the thing The sentence that started this project was five words long: "I've sent the email." It hadn't. Nothing had run. There was no email tool call in that turn, no SMTP connection, no record of anything. Just a small local m

The sentence that started this project was five words long:
"I've sent the email." It hadn't. Nothing had run. There was no email tool call in that turn, no SMTP connection, no record of anything. Just a small local model, a confident tone, and a sentence that was completely false. If you've wired a 7B model up to function calling, you've probably met the same failure. It's not a crash. It's not an exception in your logs. It's worse: it's a fluent, well-formed paragraph that silently disagrees with reality. Your UI shows a checkmark, the user moves on with their day, and the thing never happened. This is the story of the fix in my open-source project, Quasnooze, and the one design decision I'd defend hardest if you asked me why it's built the way it is.
Most discussion of hallucination is about knowledge. The model says the library was released in 2015 when it was 2017. Annoying, but visible — the user can catch it. Agentic hallucination is different in kind. When a model has tools, it doesn't just describe the world, it narrates its own effects on the world. And small models are especially prone to narrating effects that didn't happen, because:
import { auditReply, renderGuardNote } from "./src/guard.js "; const result = auditReply ({ reply: " Done — I've sent the email to priya@example.com. ", trace: [], // nothing actually ran this turn memoryFacts: [], userText: " email priya about the launch ", }); console. log ( result. flags ); console. log ( renderGuardNote ( result. flags )); Enter fullscreen mode Exit fullscreen mode Real output from that snippet:
[ { "code": "action-without-evidence", "message": "the reply says \" i've sent \" but no tool ran that could have done it" }, { "code": "invented-email", "message": "an address appears that was never given to me: priya@example.com" } ] Enter fullscreen mode Exit fullscreen mode And the note the user actually sees:
--- 🛡️ **Code check:** the reply above needs a second look. • the reply says "i've sent" but no tool ran that could have done it • an address appears that was never given to me: priya@example.com _Quasnooze checks claims against what actually ran — this one did not line up._ Enter fullscreen mode Exit fullscreen mode The detail that matters most: it never rewrites the model The tempting version of this feature silently corrects the text, or regenerates the response, or quietly drops the offending sentence. I deliberately didn't do any of that, for a reason I now feel strongly about: A silent correction is just a tidier way of lying about what happened. If the code edits the model's words, the user cannot tell whether the assistant genuinely succeeded or whether a regex papered over a failure.
So the guard appends a visible note and never touches the model's text. The transparency is the product. A flag is a nudge, not an alarm — the styling is deliberately calm, because false positives are real and I don't want to train people to panic.
The same principle — never let one layer quietly speak for another — shaped the permission system. When a tool is sensitive, the model can't approve it. Neither can a spoken "yes." Neither can an animation. Approval is a distinct path from the visual state machine, which means:
git clone https://github.com/Quasynvo/Quasnooze.git cd Quasnooze npm run setup # configure model, workspace, access key, optional channels npm run doctor # check what's actually working, including what isn't npm run brain # verify your model is reachable npm start # panel on http://localhost:7777 by default Enter fullscreen mode Exit fullscreen mode npm run doctor deserves a mention: it only probes loopback origins, and it will happily tell you that a feature is not ready rather than pretend. It's the most-used script in the repo for a reason — most "it doesn't work" issues are configuration, not code. If you want the voice visuals in your own project, the renderer is a plain ES module with no build step:
import { createVisualizer } from " /visualizer.js "; const motion = createVisualizer ( document. getElementById ( " voiceCanvas " )); // Drive this from real application state, not from a decorative timer. motion. setState ( " thinking " ); Enter fullscreen mode Exit fullscreen mode What's unfinished (please read this part too) I'd rather undersell this than have you find the gaps yourself: