EvalLayerTry the demo

Adding commercial guardrails to AutoGen agents

Published 2026-07-27 · EvalLayer

You add commercial guardrails to AutoGen agents by wrapping the tool functions themselves: the function an executor agent runs for send_email or issue_refund calls EvalLayer's /authorize endpoint first, performs the side effect only on allow, substitutes the approved rewrite when one is returned, and otherwise reports back to the conversation without sending. Because the function body is the choke point, the guardrail holds no matter which agent, chat pattern, or framework version invokes it.

Why AutoGen conversations create commercial risk

AutoGen's founding idea is agents that talk to each other: assistant and user-proxy pairs, group chats, nested conversations, with tool execution split across roles so one agent proposes a call and another executes it. The ecosystem now has two lineages, the community-run AG2 continuing the ConversableAgent line and Microsoft's rearchitected stack that fed into its Agent Framework, but both keep the same commercial exposure: somewhere in the loop, a Python function fires that emails a customer, issues a credit, or posts content, and the text it carries was negotiated by models talking to models.

Multi-agent conversation adds a specific failure mode. Agents build on each other's messages, so a concession one agent floats ("we could offer 20 percent to close this") becomes context a later agent treats as settled, and the executor faithfully ships it. The framework's native brake is human_input_mode, which can require a person at every turn, plus the reply-function chain that lets you intercept messages. Those are blunt instruments: all-or-nothing attention, with no notion of a discount cap or an approved claim. The precise instrument is a policy check at the moment of execution.

The interception point: the registered tool function

In the ConversableAgent model, a tool is registered twice: for the LLM (so the model can propose calls) and for execution (so an executor agent runs the function). Whatever proposes the call, execution funnels through your function, so wrap it there:

import httpx

POLICY = {
    "max_discount_pct": 12,
    "commitment_authority_usd": 1500,
    "forbidden_commitments": ["delivery_date", "refund_promise"],
    "approved_claims": ["gdpr_compliant"],
    "required_disclaimers": [],
    "blocked_topics": [],
}

def with_guardrail(action_type, send_fn):
    def guarded(recipient: str, body: str) -> str:
        r = httpx.post(
            "https://api.evallayer.ai/authorize",
            headers={"Authorization": "Bearer " + EVALLAYER_KEY},
            json={"action_type": action_type, "content": body,
                  "context": {"recipient": recipient}, "policy": POLICY},
            timeout=10,
        )
        data = r.json()

        if data["decision"] == "allow":
            send_fn(recipient, body)
            return "Sent. Authorization " + data["authorization_id"]
        if data["decision"] == "rewrite":
            send_fn(recipient, data["approved_rewrite"])
            return "Sent with compliant rewrite. Authorization " + data["authorization_id"]
        if data["decision"] == "require_approval":
            queue_for_human(recipient, body, data["authorization_id"])
            return "Held for human approval: " + data["violations"][0]["detail"]
        log_blocked(data["authorization_id"], data["violations"])
        return "Blocked by commercial policy: " + data["violations"][0]["detail"]
    return guarded

guarded_send = with_guardrail("send_email", real_send_email)

assistant.register_for_llm(
    name="send_email", description="Send an email to a customer"
)(guarded_send)
executor.register_for_execution(name="send_email")(guarded_send)

The registration calls above follow the ConversableAgent pattern; if you are on a different AutoGen lineage or version, register the guarded function however that surface expects. The invariant is the wrapper, not the registration syntax.

Decision handling in the conversation

  • allow: the side effect runs; the tool result confirms it with the authorization_id.
  • rewrite: the side effect runs with approved_rewrite substituted. The conversation continues; the customer gets the compliant text.
  • require_approval: nothing sends. The draft and authorization_id land in a human queue, and the returned string tells the agents the action is pending, so they can wrap up honestly instead of hallucinating success.
  • block: nothing sends. The violation detail returns as the tool result, giving the group chat what it needs to change course.

Latency notes

Wrap only functions with external side effects; calculators, retrievers, and code runners stay untouched. Deterministic-only policies (caps, authority, required text) add milliseconds. Semantic checks add one to two seconds, which disappears inside a multi-agent exchange that already spends that per turn. Set a timeout and pick failure modes per action: fail closed on refunds, fail open with logging where availability wins.

What to log

Keep the authorization_id in the tool result string (as above) so it is preserved in the conversation history AutoGen already records, and mirror it to your own store with the chat ID. Multi-agent transcripts are long; the authorization trail is the short index of every moment that could have cost money, with the verdict attached.

Try it with the demo endpoint

Wrap one function against /demo/authorize (three free checks per day, no key needed) before adding a production key, or preview the decision card at the interactive demo. For the decision model, read the authorization guide; for setting caps that match real human authority, read policy design.

Frequently asked questions

Does this work on AG2 and on Microsoft's newer AutoGen lineage?

Yes, by design. The wrapper lives in the tool function body, the one part of the stack that survived the fork: AG2 continues the ConversableAgent register-and-execute model, and Microsoft's event-driven lineage still ultimately invokes a Python function you wrote. Gate the function and the guardrail travels with it across versions.

Why not just set human_input_mode to ALWAYS?

ALWAYS makes a human approve every turn, which either buries the reviewer or gets rubber-stamped within a week; NEVER trusts the loop entirely. The authorization layer is the calibrated middle: everything is machine-checked against explicit policy, and only require_approval decisions interrupt a person.

What happens in the group chat when an action is blocked?

The tool returns a string explaining the block, which enters the conversation like any tool result. The agents can read the violation detail and route around it, propose a compliant alternative, or end the thread. Nothing external happened, and the authorization_id preserves the attempt for audit.

See the authorization layer live

Paste a message, set a policy, get allow / rewrite / require approval / block in seconds.

Try the demo