Design notes

Where to put the guardrails on an agent that acts for you

I built an agentic platform that runs my job search — a dozen specialized agents that source roles, score fit, tailor documents, and draft outreach. The model calls turned out to be the easy part. The design work was deciding what the agents may do without asking me, and then choosing where in the system that decision gets enforced.

These are the four calls I'd defend in a review. The code is private; the reasoning isn't.

1. One gate, not a check in every agent

Autonomy is a per-action-type setting — score a role, draft an email, submit an application — and every side-effectful action in the system funnels through a single gate. The gate reads the setting and either executes now or parks the action in an approvals inbox. Agents never decide their own permissions; they request, and something else adjudicates.

The part that matters most is what happens on approval: clicking approve runs the same executor with the same serialized payload the agent originally proposed. The supervised path and the autonomous path are the same code path. If they were separate implementations, they would drift, and the supervised one — exercised less — would be the one that rots.

Tradeoff: a single funnel is a bottleneck, and every new capability has to be expressed as an action type rather than just doing the thing. That friction is the feature. The alternative — each agent checking for itself — makes the safety property only as strong as the least careful agent I write at 11pm.

2. The kill switch belongs below the gate, not at it

Anything that leaves on my behalf is additionally gated on a global outbound switch, off by default. The instinct is to check that switch at the gate, next to the autonomy logic, where it reads nicely. That's wrong, and it took a near-miss to see why.

The gate is one entry point among several. An autonomous run reaches the executor through it, but so does an approval click, an API call, and the CLI. A check at the gate protects the gate's callers. A check inside the executor protects everything, including the entry point I add six months from now and forget to wire up.

Tradeoff: enforcing late means the system can do real work — compose the message, resolve the recipient — before discovering it isn't allowed to send. That's wasted compute, and I accepted it deliberately: an unwanted send is unrecoverable, and wasted work is merely annoying. When the two costs are that asymmetric, optimize against the unrecoverable one.

3. A spend cap should reuse the cancellation path

Pointing a batch agent at a personal model subscription makes one click potentially expensive, so billed calls are metered per day against a cap, checked immediately before each call — the budget refuses the next call rather than interrupting one in flight.

The decision I like is that exceeding the budget raises the same class of error as a human pressing Stop. Both mean the same thing: end the run cleanly, keep everything already finished, record why it stopped. Because looping agents already knew how to unwind a stop correctly, the budget inherited that behavior for free instead of growing its own half-tested unwind logic and a new family of partial-failure bugs.

Tradeoff: it couples two concepts that aren't obviously related, and a reader has to learn that a budget error is a stop. It earns that by deleting an entire category of bug rather than deferring it.

4. Memory is a versioned library, not a conversation

Agent knowledge lives in a typed, versioned store that every agent loads before it acts, rather than in accumulated conversation history. A dedicated curator agent proposes additions, and its proposals arrive marked as inferred — they need approval before they become something the other agents treat as true.

Conversation memory dies with the session, can't be audited, and can't be shared across surfaces. This system is driven from a web UI, a CLI, and external agents; all three need the same picture of what's true, and I need to be able to ask why an agent believed something and get an answer with a version history attached.

Tradeoff: considerably more machinery than putting history in the prompt, and the curator adds a review queue I have to actually work. It pays for itself the first time an agent confidently acts on something wrong and you need to find where that belief entered the system.

The through-line

Every one of these is the same question in a different costume: given an invariant I actually care about, what is the lowest point in the system that all callers must pass through, and is the invariant enforced there? Enforcing high up reads better and reviews faster. Enforcing at the choke point is what survives the next entry point, the next agent, and the next version of me who has forgotten the rule exists.

The related lesson is that a fallback chain should end somewhere free. When a provider is exhausted the system hops to a different subscription, then to configured alternatives, and finally to a local model — so an unattended overnight run degrades to a weaker answer instead of producing nothing. What makes that honest rather than sloppy is provenance: every result records the model that actually produced it, so a role scored by the local fallback is visibly different from one scored by the primary. Degradation is fine. Silent degradation is not.

The repository is private, but I'm happy to walk through the system live — the approvals inbox and the budget behavior are more convincing in motion than on a page.

← Back to projects