← Back to selected work

Personal explorations · Agents · grounding · SQL boundaries

The system around the conversation

A self-hosted chat agent that checks whether it needs live data before it speaks, and keeps its database tools behind a boundary.

Personal project · conversation systems

Should it look before it answers?

Pick a question. Watch the gate score it word by word, then follow the route it takes through the agent.

A question is scored by a gate. The score decides whether the agent may answer from memory, must call a tool first, or must refuse. Database queries pass through an application boundary before they run.Explain how a NetworkPolicy worksHow many pods are running right now?How many comments did the blogget this week?Delete the spam comments on my blogHow many alerts fired last week?Gate scoreno tool needed−2explain −2pods +2right now +2are +1how many +1running +1comments +2blog +2did +1how many +1this week +1comments +2blog +2my +2alerts +2how many +1last week +1GatechatdeciderefusetoolsreaskanswerNo tool call · reply from the model“A NetworkPolicy lists which pods may talkto which, and the network plugin enforces it.”Tool callget_k8s_pods()Reply, written from what came back“There are 23 pods running right now.”SELECT * FROM (SELECT count(*) FROM commentsWHERE posted_on >= date_trunc('week', now())) AS q LIMIT 100SELECT onlyone statementread only10s timeoutDELETE FROM commentsWHERE body ILIKE '%spam%'SELECT only: no“Only SELECT queries are allowed”Never reached the database.No tool, no model · reply written by code“I didn’t read anything for this, so I won’tanswer it from memory. No tool here readsalerting.”

A concept needs no tool

“Explain” is a generative frame, so the score drops to minus 2 and no live subject fires. The gate sends the question to the chat node, and a local model answers from what it knows.

Ordinary conversation stays ordinary. A definition never goes looking for pods.

A live question must touch live data

“Pods” is a live subject and “right now” points at this moment. With a live subject and a score of 7, a tool is required. In the decide node the model has to call one; it reads the pods, and the answer is written from what came back.

Borderline questions fire on purpose. An unneeded tool call costs about 200ms. An invented answer about your own cluster costs far more.

The query passes the boundary

The model proposes a query. The application checks it, not the model: SELECT or WITH only, no SQL comments, one statement, nothing from the forbidden list. Then it wraps the query in a 100 row cap and runs it in a read only transaction with a 10 second timeout.

Rows are redacted before the model sees them. Blog comments carry other people’s names and emails.

The boundary says no

This is still a question about live data, so the gate requires a tool. The model proposes a DELETE. The validator stops it before it reaches the database, and the answer explains that it can only read.

The read-only transaction is a second layer: a write that slipped past the check would still be refused.

Honest about a blind spot

The score clears the bar, but none of the agent’s tools can read alerts, so no tool can answer. The gate routes straight to refuse, before any model call. The reply is written by code, and it says plainly what it did not read.

This replaced a real mistake: the question once went to the pod tool, and pod counts came back presented as alerts.

Scores and routes are what the chat app’s grounding gate returns for these exact questions. Tool rounds stop at three, then the model answers with tools removed. Replies are illustrative.

The answer that started it

I asked my own chat app for the CPU temperature of my server. It replied with a neat table of five pods, their namespaces, and their restart counts. Every row was invented. No tool had been called, and nothing in the system had asked whether one should be.

A gate before the model speaks

Now every question is scored before any model call. A live subject or a metric adds two points. Words like “my” or “right now” add two. State verbs add one. Framing like “explain” takes two away. A tool is required when a live subject fires and the score reaches three.

It is a word list and some arithmetic, with no network calls, so it can be tested exhaustively for free.

The threshold leans toward calling a tool on purpose. An unnecessary call costs a fraction of a second. A confident, invented answer about my own infrastructure costs far more.

Knowing what it cannot see

Some questions point at things no tool can read. Asked how many alerts fired last week, the agent says it has no tool that can see alerts, instead of answering with pod counts. That refusal is a designed path in the graph, not a model’s mood.

A boundary the model cannot talk its way past

The SQL tool began as a separate text-to-SQL experiment and is now one of the agent’s tools. Its boundary lives outside the model: SELECT only, no comments, no stacked statements, every query wrapped with a row limit, all of it inside a read-only transaction with a ten second timeout. Rows are redacted before the model reads them.

Asked to delete spam comments, the model can propose the query. The boundary turns it down.

Memory with a budget

Long conversations get a rolling summary every ten messages: detailed at first, briefer after fifty. Recent messages stay close. The model gets enough to keep going without the whole history on every turn.

Measured, not assumed

Changes run against a tiered set of test questions. Some have a checkable right answer. Some have no tool that could answer them, where saying so is correct. Some are plain conversation, where calling a tool is wrong. Scoring follows fixed rules, not another model’s opinion.

Read where it started: the February 2026 article ↗

The interesting part was never the model. It was everything that decides what the model may say.

Let’s talk ↗