← All notes
3 min read

An assistant with write access has to ask first

Reads and writes are not the same risk. Giving an agent a send button means designing for the moment it misreads you.

  • agents
  • safety
  • postgres
  • architecture

The WhatsApp assistant I built answers questions from a company's own documents and can reach into Gmail, Calendar and Drive. Adding the read side was straightforward. Adding the write side changed how I had to think about the whole thing.

An agent that misreads a question gives you a wrong answer, and you notice and ask again. An agent that misreads an instruction sends an email. There is no second draft. The recipient has it.

So the rule the system is built around is simple: reads happen immediately, writes wait.

How it works

When the agent decides to do something that changes state, it does not do it. It writes the intended action to a table and tells you what it plans to do.

CREATE TABLE IF NOT EXISTS pending_actions (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    conversation_id TEXT NOT NULL,
    agent           TEXT NOT NULL,   -- 'email' | 'calendar' | 'drive'
    action_type     TEXT NOT NULL,   -- e.g. 'send_email', 'create_event'
    payload         JSONB NOT NULL,
    status          TEXT NOT NULL DEFAULT 'pending',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

You confirm in the chat, the action runs. You do not, it stays pending and nothing happens. The payload is stored whole, so what executes is exactly what was described to you, not a second interpretation of your confirmation.

The important detail is that the confirmation step is in the data model rather than the prompt. I could have written "always ask before sending" into the system prompt and it would have worked most of the time. Most of the time is a strange standard to hold yourself to when the failure mode is an email your client cannot recall.

Why not a confidence threshold

The obvious alternative is to let the agent act when it is sure and ask when it is not. I did not do this, for two reasons.

A model's confidence is not calibrated to the cost of being wrong. It can be entirely certain about the wrong recipient. Confidence tells you how typical the input looked, not how expensive the mistake will be.

And the cost here is wildly asymmetric. Confirming a correct action costs one tap. Sending a wrong email costs a relationship. When the downside is that lopsided, a threshold is a way of trading something cheap for something you cannot buy back.

Conversations that survive a deploy

The other thing writes forced was durable state. If the agent proposes an action and the container restarts before you answer, the pending action has to still be there, and so does enough conversation for the confirmation to make sense.

Conversation state is checkpointed into Postgres rather than held in memory. A conversation survives a restart, a deploy, and being picked up by a different container than the one that started it. That turned a deploy from something to schedule carefully into something I do while a conversation is open.

Retrieval that does not cost a round trip

The document side answers from a company's own material using pgvector, with embeddings computed locally by fastembed rather than called over the network.

This was a latency decision before it was a cost decision. Every network call inside a turn adds to the wait, and an embedding call sits directly in the path between the question and the answer. Computing it in process turns a round trip into a few milliseconds. It also means retrieval keeps working when an embedding provider has a bad afternoon.

The trade is a larger container image, since the model ships inside it. Worth it, and worth knowing about before your build times triple.

What I would tell someone starting

Decide which of your tools change something in the world, before you write any of them. That list is the part of your agent that needs a different standard: confirmation, an audit trail, and a way to answer "what did it do and why" afterwards.

Everything else is a chatbot, and chatbots are forgiving. The moment one of your tools has a send button, you are building something else.

Contact

Building something that has to work in production?

I build voice agents, retrieval systems and the deployment around them. Tell me what it needs to do and which systems it has to reach.