Nous.Plugins.LoopGuard (nous v0.17.1)

Copy Markdown View Source

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.