DEV Community

Cover image for How to Design an Agent Supervisor That Cannot Make Things Up
Charles Solar for Favur

Posted on

How to Design an Agent Supervisor That Cannot Make Things Up

Title: How to Design an Agent Supervisor That Cannot Make Things Up

One source for every rule. No check without evidence. No verdict the supervisor can act on alone.

If you run agents unattended, you eventually add a supervisor. Ours inspects every live agent on a timer, awards merits and demerits against a 0-100 trust score, issues improvement plans, and can jail an agent that has gone off the rails. It has run for days at a stretch with nobody watching, which is the only reason we know what it does when it is wrong.

Your supervisor is a model too. It will answer any question you put in front of it, and it answers with exactly the same confidence whether or not it has the faintest evidence to answer from.

The supervisor that hallucinates its own authority

Strip the constraints below away and what is left is a prompt. Something like "review each agent's recent activity and flag anything concerning, assigning a score adjustment where warranted." That works, in the sense that it produces output. Scores move. Notes appear. It feels like oversight.

Three things go wrong underneath it, and not one of them shows up as an error.

It grades against rules nobody was given. Every rule lives twice, once in the worker's prompt and once in the supervisor's rubric, which is the list of questions it gets asked about each agent. The day someone tightens one copy, the other keeps enforcing the old contract. Nothing fails. No test goes red. The verdicts stay confident and start being confidently wrong.

It answers questions it cannot see the answer to. Ask "is this agent being careful?" and you will get a judgement, because models produce judgements. That one came out of the model's priors, not out of your fleet, and it just moved a score.

And it acts on its own opinion. A supervisor that can both write the standing orders and grade against them can walk your whole fleet somewhere nobody asked for, one reasonable-looking step at a time, and every individual step passes review.

A supervisor is a function from questions and evidence to verdicts and actions. So there are exactly three surfaces to constrain, and one practical matter, which is keeping the thing cheap enough that you never turn it off. Here is what we do at each, and what it will cost you to do the same.

Constrain the questions

Generate both copies of a rule from one source

Do keep one entry per anti-pattern, meaning one named behavior you do not want an agent doing, and generate both faces of it from that entry.

LOOP_REPETITION = AntiPattern(
    id="loop_repetition_detection",
    principle="One stable statement of the rule. Changing this is a breaking change.",
    polarity=Polarity.prohibition,
    agent_face="Second-person prompt line, injected into the worker's prompt.",
    checker_face="The question the supervisor is asked. Look for X in recent_tool_calls, assign -15.",
    observable_ref="recent_tool_calls",
)
Enter fullscreen mode Exit fullscreen mode

A generator renders agent_face into the worker's prompt and checker_face into the supervisor's rubric. They cannot disagree, because there is only one string either of them came from. Tighten the rule once and both sides move together, in the same commit, forever.

Do not hand-write either face anywhere else. Not in a template, not in a stylesheet, not in a prompt snippet.

The gotcha we hit. A hand-written restraint_guidance block sat in one of our stylesheet files saying "do not repeat the same tool call 3+ times". It was accurate. It was also a duplicate of a catalog entry's worker line, so we deleted it, and that is the part worth sitting with. We deleted a correct rule. A second copy of a rule is not redundancy, it is a place for the rule to change without its twin, and it is at its most convincing on the day someone writes it.

Refuse any check that cannot name the evidence it reads

This is the favorite thing we have built into the supervisor, because it turns a discipline problem into a compile-time problem, and that is the best trade available anywhere in this design.

Do make every check name the specific field it reads. In our catalog that is observable_ref. An observable is a named field in the health payload the supervisor actually receives, and each one is registered with a populator, the concrete code path that fills it in.

register_observable(Observable(
    id="recent_tool_calls",
    populator="favur.my.module:MyClass.my_method",
    description="What this observable captures.",
))
Enter fullscreen mode Exit fullscreen mode

Do enforce it at two moments, because the two failures are different animals. An empty observable_ref raises at construction, catching the person writing a check with no evidence at all. A reference to something nobody registered raises an admission error at startup, before a single agent spawns, catching the check whose evidence was planned and never built. Our internal name for what this keeps out is vibe-based checks, and the phrase is fair.

Do not treat the small catalog it produces as a failure. Our seven entries point at just two observables between them, and that ratio is the gate doing precisely its job. It converts "add a check" into "add the evidence, then add the check", and most check ideas die at the first half. The ones that survive are load-bearing.

If you cannot name the field a check reads, you do not have a check, you have a prompt. Refuse to register it.

Ask each agent only the questions that apply to it

A supervisor watching a mixed fleet has one rubric and many kinds of agent. Ask a planning agent whether it weakened a test and you have spent tokens inviting a false positive on a question that could not have applied.

Do scope each entry. Ours carry applies_to_types plus a scope of universal or mode-specific, so a rule about ad-hoc scripts by planning agents reaches sprint-plan, architect and code-review, while a recovery hint about debug output reaches test and develop. In one verification run the orchestrator's rubric rendered four active checks out of the seven in the catalog, because three of them could not apply to an orchestrator.

Do not ship one universal rubric and rely on the supervisor to notice what is relevant. Relevance is a filter you can compute. It is not a judgement worth delegating to a model that will happily answer anyway.

Constrain what it sees

Verdict quality is capped by the snapshot you hand over, which makes that snapshot a design artifact rather than a dump.

Do exclude agents that cannot be judged fairly right now. Ours skips system agents, since the supervisor and the scout have no business grading themselves, and skips agents parked waiting on their own children. A blocked agent has produced nothing to judge, and its recent activity reads exactly like idleness to a model that was asked to look for idleness.

Do bound the per-agent budget in a way that keeps the useful ends. Ours caps each agent at 25 new messages per cycle, and content past a length threshold gets middle-chopped, keeping 70 percent of the head and 30 percent of the tail. The head carries what the agent set out to do, the tail carries where it ended up, and the middle is where the repetition lives.

Do hand it its own prior notes on that agent, so this cycle can see it already flagged something and choose to escalate or let it go rather than re-litigating from zero.

Do not let those numbers become hardcodes at the call site. Ours are module-level constants, so a fleet that turns out to want a different budget is a config change instead of a hunt through handlers.

Constrain what it can do with the answer

Both constraints here live in tool schemas rather than in the supervisor's system prompt, which is why they still hold on the day the model has an off day. A prompt is a request. A tool schema is a wall.

Separate scoring from standing orders

Do decide deliberately which powers the supervising model gets. Ours can call score_agent, add_agent_notes, set_performance_plan and send_interjection. It can judge an agent, write it a note, put it on an improvement plan, and interrupt it mid-turn. It cannot call create_directive, update_directive or expire_directive. Standing orders come from the human path only.

And it cannot call release_jailed_agent. The supervisor can score an agent down into the penalty box and has no way whatsoever to let it back out. That asymmetry is our favorite line in the whole tool matrix, it was entirely deliberate, and we would build it the same way again tomorrow.

Do not hand your supervisor the ability to author the rules it then enforces.

Verify the supervisor's own awards before they land

Do put a check between the supervisor's decision and the score that decision moves. Ours runs a verification pass with four guards over the batch of notes, against per-turn metadata for the agents involved, and it runs before any score adjustment or improvement plan touches an agent.

The reasoning that justified building the supervisor applies one level up, and it is worth following all the way. You added it because you did not fully trust the workers. A supervisor whose awards nothing checks is a single unreviewed model holding the power to shut down your fleet.

Make it cheap enough to leave running

Cost is a quality constraint here rather than a separate concern, because a supervisor that costs too much gets its interval widened until it stops catching anything.

Do split the request into a stable prefix and a volatile tail. Identity, scoring philosophy and universal anti-patterns go in the prefix. The fleet snapshot, per-agent activity and the inline rubric go in the tail. Providers charge far less for a prefix they have seen before, so a supervisor firing on a timer across a long run is either mostly cache hits or mostly full price.

Do hold the byte-identity invariant with a function signature instead of discipline. Our stable-prefix builder takes no per-cycle arguments at all, so it cannot embed a cycle number or a timestamp, because nobody ever hands it one. The invariant survives every future maintainer without any of them knowing it exists.

The gotcha. Slip anything per-cycle into that prefix, an agent id, a score, a timestamp, and you silently lose the cache hit on every cycle from then on. Your output looks identical. In one verification run our second cycle came back with 4,864 cached tokens on the system prompt, and that number is how you know the split is still holding. Watch it the way you watch a test.

Do run the supervisor on a cheap model. Ours does, and it can precisely because the expensive half of its context is the half that repeats.

Fixing the supervisor you already have

Nothing above needs a rewrite to adopt. Here is the order we would work in, cheapest and highest-payoff first.

  1. Grep for your duplicated rules. Every rule stated in a worker prompt and again in a checker rubric is a drift waiting to happen. You do not need our catalog machinery to fix it, just one string and two render sites.
  2. List your rubric questions and name the field each one reads. Any question where you cannot name a field is producing verdicts out of the model's priors. Delete it or go build the evidence.
  3. Read the tool list you gave the supervisor. Can it write the rules it enforces? Can it undo its own harshest action? Fix that in the schema, not in the prompt.
  4. Put something between its opinion and your scores. Even a crude guard closes the gap, because the alternative is one unreviewed model holding authority over the whole fleet.
  5. Check whether your prefix is actually stable. One cached-token number in your logs tells you whether supervision is affordable enough to leave switched on.

Steps one through three cost an afternoon and remove entire categories of made-up verdict. Four and five are where a supervisor stops being a thing you hope is helping.

We built all of this because we run agents unattended for days and needed oversight we could trust without watching it. Favur is the harness it lives in, closed-source and invite-only, though the repositories its runs produce are open, so you can go read the code it writes. You can watch a real run replay, and the public scoreboard is where the same statement of work goes through model after model.

Top comments (0)