---
title: "Agent Guardrails"
---

An autonomous agent decides its own actions at runtime, which is what makes it useful — and what makes it risky. The common failure modes are mundane: it loops, repeating the same call without making progress; it runs away, taking far more steps (and cost) than the task warrants; it takes an action that should have had a human or a policy in the way.

Go Micro separates **orchestration** (the model deciding what to do) from **execution safety** (whether a decided action is allowed to run). Every tool call an agent makes passes through one choke point, and that's where the guardrails live — so they apply uniformly to service calls, custom tools, and `delegate`, without touching the model or your services.

## The three agent guardrails

### Stop on count — `MaxSteps`

Bounds the total number of tool executions in a single `Ask`. Once exceeded, further calls are refused and the model is told to stop and summarize. The blunt backstop against runaway cost.

```go
micro.NewAgent("worker", micro.AgentMaxSteps(8))
```

### Stop on repeat — `LoopLimit`

Bounds how many times the agent may call the **same tool with the same arguments** in one `Ask`. Identical repeated calls make no progress — `MaxSteps` only bounds them by total count, and a circuit breaker only catches *failures*, not a call that succeeds and is pointlessly repeated. When the limit is hit, the call is refused with a message that tells the model it's looping, so it changes approach instead of spinning:

> loop detected: you have already called "search.Search.Query" with the same arguments 3 times and the result will not change. Stop repeating it — try a different approach, or finish with what you have.

```go
micro.NewAgent("worker", micro.AgentLoopLimit(3))
```

`LoopLimit` is **on by default** (a lenient 3) because identical repeated calls are never useful. Set `AgentLoopLimit(0)` to disable it.

### Gate the action — `ApproveTool`

A hook called before each action runs. Return `false` to block it, with a reason that's surfaced to the model. Use it for human-in-the-loop approval, spend limits, allow/deny lists, or any policy:

```go
micro.NewAgent("worker", micro.AgentApproveTool(
    func(tool string, input map[string]any) (bool, string) {
        if strings.HasPrefix(tool, "billing_") {
            return false, "billing actions require sign-off"
        }
        return true, ""
    }))
```

## ApproveTool is the integration seam

`ApproveTool` is also where an **external policy engine** plugs in. It sees every tool call before execution and can veto, so you can route decisions to your own rules, a budget service, or a third-party runtime-safety layer — without go-micro depending on it. Orchestration stays in the agent; execution safety stays in the hook. That separation is the whole point: you can swap the safety layer without touching the agent.

## Wrap the whole execution — `WrapTool`

`ApproveTool` is a *before* gate. When you need the full lifecycle — timing, logging, metrics, retries, or inspecting the result — wrap the execution instead. `WrapTool` is the tool-side analogue of go-micro's `client.CallWrapper` and `server.HandlerWrapper`: a wrapper takes the next handler and returns a new one, so code before the `next(...)` call runs *before* the tool, and code after runs *after*.

```go
import "go-micro.dev/v6/ai"

func logging(next ai.ToolHandler) ai.ToolHandler {
    return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
        start := time.Now()
        res := next(ctx, call)
        log.Printf("id=%s tool=%s took=%s", call.ID, call.Name, time.Since(start))
        return res
    }
}

micro.NewAgent("worker", micro.AgentWrapTool(logging))
```

The handler signature is the same one every provider uses to execute a tool, and it mirrors a service handler — context first, the call in, a result out:

```go
type ToolHandler func(ctx context.Context, call ToolCall) ToolResult
type ToolWrapper func(ToolHandler) ToolHandler
```

`call.ID` is a correlation ID carried through from the provider, so a wrapper can tie a tool call back to the request it came from. `call.Scan(&v)` decodes the arguments into a typed struct when you'd rather not work with the raw map.

Wrappers run **outside** the built-in guardrails, so they observe every call and its result — including a guardrail's refusal. Multiple wrappers compose outermost-first (the first registered is the outer layer). A "before/after" hook is just the two halves of one wrapper, and retry is calling `next` again — so the wrapper is the single, composable seam for everything around execution, while `MaxSteps`, `LoopLimit`, and `ApproveTool` remain the named guardrails on top of it.

### Reliability metadata

A wrapper has what it needs to build reliability tooling — loop handling, retry policies, auditing — without coupling to the agent:

- **What happened** — a guardrail refusal is tagged with a structured reason on the result, so you switch on it rather than parse a message:

  ```go
  res := next(ctx, call)
  switch res.Refused {
  case ai.RefusedLoop:     // the agent repeated an identical call
  case ai.RefusedMaxSteps: // the step budget was exhausted
  case ai.RefusedApproval: // ApproveTool blocked it
  }
  ```

- **Which run** — `ai.RunInfoFrom(ctx)` returns a correlation id for the run, the agent's name, and the parent run when the call came from a delegated sub-agent:

  ```go
  if run, ok := ai.RunInfoFrom(ctx); ok {
      log.Printf("run=%s parent=%s agent=%s tool=%s", run.RunID, run.ParentID, run.Agent, call.Name)
  }
  ```

- **Per-call detail** — `call.ID` (correlation), `call.Name`; duration is `time.Since(start)` around `next`, and step/attempt counts are naturally counted by the wrapper itself (it sees every call).

## Execution safety at the gateway

When agents reach tools **through the MCP gateway**, the gateway adds its own per-tool policies, independent of the agent:

- **`RateLimit`** — requests-per-second per tool.
- **`CircuitBreaker`** — a tool that fails repeatedly is temporarily blocked, so a failing dependency doesn't cascade.

Together with the agent-side guardrails, that's a full set: bound the count, stop the spin, gate the action, rate-limit and circuit-break at the edge.

## Why it matters for autonomous agents

These are most important when no human is in the loop. An agent [triggered by an event](/blog/2026/06/15/when-the-event-is-the-prompt.html) runs unattended — there's no one to notice it looping or to approve a risky call. The guardrails are what let it fail safely and recover on its own rather than quietly burning resources.

## See also

- [Plan & Delegate](plan-delegate.md) — the agent's built-in tools
- [Agents and Workflows](agents-and-workflows.md) — where agents fit
