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.MemoryandNous.Plugins.KnowledgeBaseput 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.