/ 2min

An agent that edits files should ask first

Buffering every write until a human approves the plan, and why the approval gate belongs in the runtime rather than the prompt.

Prompt-level guardrails are suggestions. If the only thing standing between an agent and a user’s project files is an instruction in the system prompt, then a sufficiently confused model will eventually write something nobody approved. For a builder tool where the agent edits a real workspace, that is not an acceptable failure mode.

The fix is not a better prompt. It is moving the gate out of the model’s reach.

Two phases, one transcript

Each chat turn rebuilds the agent from the stored transcript, which keeps the runtime stateless between turns and makes a run reproducible. The turn then runs in two phases.

In the planning phase the agent can read anything and write nothing. File writes are buffered in memory as proposed changes, not applied to disk. When the agent finishes planning, the buffer is presented to the user as a plan.

In the execution phase, which only begins after approval, the buffered writes are flushed and new writes are allowed through. A rejected plan discards the buffer, and the project is exactly as it was.

if (!approved) {
  buffer.discard();
  return { status: "rejected" };
}
buffer.flush(); // one history entry, not one per file

The flush is a single entry in the project history. That matters more than it looks: if each file write became its own undo step, reverting a rejected-but-partially-applied run would mean clicking undo a dozen times and hoping nothing was missed.

Metering that adds up

The second place where a permissive design bites is spend. Credits are integers, token usage is not, and converting between them at the wrong moment produces either rounding drift or fractional credits that never quite reach zero.

The approach here is an additive usage ledger. Every run appends its measured usage, and the meter settles credits from the sum rather than mutating a balance in place. Drift stops being a slow leak and becomes something you can recompute from the ledger at any time.

The same lesson as the approval gate, really. When a number or a file matters, derive it from a record of what actually happened, not from a value that was adjusted in place and can no longer be audited.

Portability is a constraint, not a feature

One more decision shaped the schema: the same codebase runs on PostgreSQL and SQLite. That rules out most of the conveniences you would reach for on a single database: no partial indexes with different syntax, no dialect-specific JSON operators in the hot path.

Writing to the intersection is slower to design and much faster to deploy, because the thing you tested locally is the thing that runs in the small deployment, and the thing that runs in the small deployment is the thing you can hand to a user who wants to self-host.

More posts

2 min

Failover without breaking the stream

Moving a request to a healthy provider is easy. Moving it after the first token has already reached the client is the hard part.

  • Rust
  • Streaming
  • Reliability
Read article

2 min

One route, five windows

How a single SvelteKit route became a workspace, a dock, a desktop-locked board, and two on-demand windows, all reading the same local database.

  • Tauri
  • Svelte
  • Desktop
Read article