# `Nous.Plugins.LoopGuard`
[🔗](https://github.com/nyo16/nous/blob/v0.17.3/lib/nous/plugins/loop_guard.ex#L1)

Plugin that notices an agent repeating the same tool call and injects
escalating guidance.

Opt-in. Nothing changes for an agent that does not list it:

    agent = Nous.new("openai:gpt-4o", plugins: [Nous.Plugins.LoopGuard])

A "repeat" is a consecutive run of assistant messages making the same
`{tool_name, arguments}` call, comparing arguments by canonical form so key
order and atom-vs-string keys cannot defeat the match. At each configured
threshold the plugin appends one advisory message to the transcript. It never
blocks the call, never edits a tool result, and never ends the run — a looping
model is told, not stopped, because blocking one is a separate decision with no
evidence behind it yet.

## Configuration (via `deps[:loop_guard_config]`)

  * `:thresholds` — chain lengths that trigger an advisory. Default `[3, 5, 8]`.
    Sorted and de-duplicated; the Nth threshold gets the Nth wording tier, and a
    list longer than the three tiers reuses the firmest one.
  * `:excluded_tools` — tool names that are transparent to the chain: they
    neither increment it nor reset it, so a bookkeeping call (a todo write, a
    progress ping) cannot launder a loop by sitting between two identical
    calls. Default `[]`; matched case-insensitively. Nothing is excluded by
    default because this repo ships no bookkeeping tool to name.

`:deps` is passed to `Nous.run/3`, not `Nous.new/2`:

    {:ok, result} =
      Nous.run(agent, "...",
        deps: %{loop_guard_config: %{thresholds: [4, 8], excluded_tools: ["todo_write"]}}
      )

## What counts

  * Denied calls count. A call blocked by the permission policy, an approval
    handler or a hook still appears in the assistant message that requested it,
    and a model retrying a call it is not allowed to make is exactly the loop
    worth catching. The chain is read off the requests, never off the results.
  * A new user message resets the chain — it is a new instruction.
  * A tool result does not: it is the echo of a call already counted from the
    assistant message that made it.
  * An injected system message does not either. `Nous.Plugins.Memory` and
    `Nous.Plugins.KnowledgeBase` put context there mid-conversation, and letting
    an unrelated plugin's injection reset the chain would make the guard
    silently depend on the plugin list.
  * An assistant message with no tool calls resets the chain: the model answered
    instead of calling, so the run of identical calls is over.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
