The harness watchdog¶
The watchdog is an optional safety net you turn on for a personality. While the
agent works, the watchdog watches for signs that the agent is stuck in a loop,
and when it sees one, the watchdog drops a short nudge into the conversation to
break the agent out. The nudge is an ordinary message, tagged [harness] to mark
it apart from your own messages. The agent is free to ignore the nudge and keep
going.
The watchdog is off unless you turn it on. A fresh setup never nudges, because
no personality watches until you add a watchdog to one. The built-in default
personality watches only once you set a default policy for it. It is the
personality active when you have selected no personality of your own.
Turns and rounds¶
Two words matter for the settings below.
A turn is everything the agent does in response to one prompt you send. The agent may think, call a tool, read the result, think again, call another tool, and so on, until it has an answer for you and hands control back. One prompt from you is one turn.
A round is a model response inside a turn. A turn usually contains several rounds: the model responds, tools run, the model responds again with the results in hand. The turn ends when a response comes back with no tool calls.
The counts below (rounds without progress, nudges allowed per turn, and so on) are measured within a turn, and they start fresh each time you send a new prompt.
Enabling it¶
Add a watchdog table to a personality in your Lua config. Adding the key turns
the watchdog on. An empty table turns it on with every default:
Deleting the key turns the watchdog off again. The watchdog re-reads its
settings at the start of every turn. Because of that, switching to this
personality with /personality turns the watchdog on, switching away turns it
off, and /reload picks up any edits you make to the table right away.
The default personality¶
The built-in default personality takes no watchdog key of its own. To watch
it, set a default policy from a reconfigure handler:
An empty table enables it with every default. The keys are the same as a
personality watchdog table. This policy governs only the default personality.
A named personality never inherits it, and opts in through its own watchdog
key. A named personality may take these same settings explicitly with
agent.default_watchdog():
registry:add {
name = 'Executive',
model = 'beast/qwen38-27b',
watchdog = agent.default_watchdog(),
}
When no default policy is set, agent.default_watchdog() reads as nil, which
leaves that personality's watchdog off.
Sub-agents¶
A spawned sub-agent runs its own watchdog, driven by the watchdog key of the
sub-agent's own personality, with counts independent of the parent and of any
other sub-agent. A sub-agent always runs under a named personality, which has no
default policy to fall back to. It watches only when that personality declares a
watchdog.
What it watches for¶
The watchdog has four independent triggers. Each one watches for a different
shape of loop, and one of them can fire a nudge. The examples below sketch what
each loop looks like in the TUI. The » watchdog: line is the nudge.
A tool that keeps failing¶
The agent calls a tool, the call fails, and the agent tries the same thing
again. After max_consecutive_errors failures in a row (3 by default) with no
success in between, the watchdog nudges. One successful call clears the count.
you> get the test suite green
agent> runs: cargo test -> error[E0433]: failed to resolve
agent> runs: cargo test -> error[E0433]: failed to resolve
agent> runs: cargo test -> error[E0433]: failed to resolve
» watchdog: Perhaps it is time to ask for help?
Rounds that don't make progress¶
The agent keeps working but never completes a successful tool call. After
max_rounds_without_success rounds in a row (4 by default) end with no
successful result, the watchdog nudges. A round in which the agent finishes and
hands control back to you does not count, because stopping on purpose is not
spinning.
you> find who calls parse_header
agent> greps: "parse_header" in src/ -> no matches
agent> greps: "parseHeader" in src/ -> no matches
agent> greps: "parse_hdr" in src/ -> no matches
agent> greps: "header(" in src/ -> no matches
» watchdog: Perhaps it is time to ask for help?
The same tool calls over and over¶
The agent repeats the same sequence of tool calls even though every call
succeeds. Neither trigger above notices this, because nothing fails and each
round makes a "success". The watchdog looks at the last repeat_call_window
tool calls (20 by default) and nudges when the same group of calls has repeated
repeat_call_threshold times (4 by default).
you> summarize the config module
agent> reads: src/config.rs -> ok
greps: "load_config" -> ok
agent> reads: src/config.rs -> ok
greps: "load_config" -> ok
agent> reads: src/config.rs -> ok
greps: "load_config" -> ok
agent> reads: src/config.rs -> ok
greps: "load_config" -> ok
» watchdog: Perhaps it is time to ask for help?
Thinking in circles¶
The model keeps saying the same thing, in its thinking or in its visible replies, whether or not it is calling tools in between. This is the one trigger that sends the break-the-loop nudge instead of the escalation nudge:
you> refactor the parser
agent> (thinking) I should start by understanding the existing parser first.
agent> (thinking) Let me begin by getting a handle on how the parser works.
agent> (thinking) First I need to understand the current parser.
... the same thought, reworded, keeps coming back ...
» watchdog: You have been repeating yourself. Decide on a next step or state your conclusion.
The detector matches at two levels. A strict match is the same sentence
almost word for word, and it fires after repeat_sentence_threshold repeats (4
by default). A loose match is the same idea in different words, like the
three rephrasings above, and it fires after the higher
repeat_sentence_threshold_loose count (6 by default). To avoid nudging on a
brief thought, the detector ignores sentences shorter than min_sentence_chars
characters and does not fire until the model has produced min_armed_sentences
sentences in the turn.
How the triggers work together¶
A few rules govern all four triggers simultaneously:
- The first three triggers send the escalation nudge (
nudge_text). Only thinking repetition sends the break-the-loop nudge (break_loop_text). - After any nudge, the watchdog waits before nudging again. It always waits
refractory_seconds(30 by default). Once a round has finished since the nudge, it also waitsrefractory_roundsmore rounds (2 by default). A nudge that fires in the middle of a round, such as a thinking nudge, is held by the seconds only, because no round has finished yet for the round count to apply. - The watchdog does not fire while the agent has a sub-agent running, since the agent is already reaching for help. Meanwhile the sub-agent supervises itself with a separate watchdog, driven by the sub-agent's own personality policy.
- The break-the-loop nudge is limited to
max_think_nudgesper turn (2 by default). When the agent hits that limit and keeps looping, the watchdog interrupts the turn outright (interrupt_on_cap, on by default). Setinterrupt_on_cap = falseto let the turn keep running instead. The other three triggers have no per-turn limit. - The watchdog remembers its counts per session, and it forgets them when you switch personality, so a streak on one personality never transfers into another.
What the nudge looks like¶
The nudge goes into the conversation as a message from you, tagged [harness],
to let the model read it as part of the dialogue and answer it. In the TUI the
same nudge also shows as a dim one-line notice, set apart from your own
messages:
That notice is only a display of the nudge. The message itself is stored in the conversation history.
Configuration reference¶
Every key is optional, and an omitted key takes its default. All thresholds must be at least 1.
| Key | Meaning | Default | When to adjust |
|---|---|---|---|
max_consecutive_errors |
Failing tool calls in a row that fire the nudge | 3 | Lower to break out of a failing tool sooner. Raise if a tool that legitimately fails a few times in a row is getting nudged. |
max_rounds_without_success |
Rounds in a row with no successful tool result that fire the nudge | 4 | Lower for an earlier nudge on stuck work. Raise if long tool-heavy work with no clear "success" is getting nudged. |
repeat_call_window |
How many recent tool calls the detector looks back over | 20 | Raise to hold repeat_call_threshold * repeat_call_max_len calls in the window. Must be at least that product. |
repeat_call_threshold |
How many times a group of calls must repeat before the nudge | 4 | Lower to catch a loop sooner. Raise if a legitimately repeated sequence is getting nudged. |
repeat_call_max_len |
The longest run of calls treated as one repeating group | 4 | Raise if the repeating group is longer than four calls. Must be at least 2. |
repeat_sentence_window |
How many recent sentences the thinking detector looks back over | 50 | Raise if the repeated thought is spread out among other thinking. |
repeat_sentence_threshold |
Repeats of a near-identical sentence that fire the strict match | 4 | Raise if the model restates itself legitimately and gets nudged. Lower to catch a loop sooner. |
repeat_sentence_threshold_loose |
Repeats of the same reworded idea that fire the loose match | 6 | Raise if a reworded but legitimate thought is getting nudged. Must be greater than repeat_sentence_threshold. |
min_sentence_chars |
Sentences shorter than this are ignored | 20 | Raise to ignore short boilerplate like "Let me check." |
min_armed_sentences |
Sentences the model must produce in a turn before the thinking detector can fire | 10 | Raise if short bursts of thinking are getting nudged. Lower to catch a loop that starts early. |
max_think_nudges |
Break-the-loop nudges allowed per turn before the limit kicks in | 2 | Raise to provide more nudges to the agent before the turn is cut. Lower to give up sooner. |
interrupt_on_cap |
Interrupt the turn once max_think_nudges is reached |
true | Set to false to let the turn keep running past the limit instead of being cut. |
refractory_rounds |
Rounds that must pass after a nudge, once a round has finished, before the next nudge | 2 | Raise if a second nudge follows the first too soon. |
refractory_seconds |
Wall-clock seconds that must pass after a nudge before the next one | 30 | Raise to space nudges further apart on a slow, long-running turn. |
nudge_text |
The escalation nudge text, used as written | "Perhaps it is time to ask for help?" | Change to steer the agent a different way, such as toward asking you a question. |
break_loop_text |
The break-the-loop nudge text, used as written | "You have been repeating yourself. Decide on a next step or state your conclusion." | Change to name the specific loop you want the agent to drop. |
Here is a personality that keeps the defaults but nudges sooner and speaks to the agent in its own words:
registry:add {
name = 'Executive',
model = 'beast/qwen38-27b',
watchdog = {
max_consecutive_errors = 2,
max_rounds_without_success = 3,
nudge_text = 'Stop and tell me what is blocking you.',
break_loop_text = 'You are going in circles. Pick one next step.',
},
}
If you mistype a key or give a threshold a non-integer value, the config fails to load and the error points at the offending line, rather than the mistake being ignored.