Loop safety
Agents fail in characteristic shapes: retrying the same call forever, drifting off the objective, grinding without progress, or churning through steps that surface nothing new. This pattern detects those shapes with run signals and breaks them with circuit_break — a reset that snapshots working memory to a durable entry before clearing it, so recovery costs a reset, not the run's accumulated state. Use it in any orchestrator that supervises long-running or unattended agents.
How it works
Detection is advisory; action is yours. Mubit's monitors flag pathologies but never act on them — no server-side abort, no automatic reset. Two read surfaces:
- Per-recall signals — every
recall()response carries a lightweightsignalsblock:repeated(same normalized query over threshold),stagnant(negative-outcome streak),drift_score,novelty_score. Free to read; check it in the agent's own loop. - The run signal — the run-monitor op re-inspects the run on demand and returns a merged verdict for dashboards and supervisors:
is_looping,is_drifting,is_stuck,is_novelty_starved, plusloop_score,drift_score, a human-readablerationale, aconfidence, and thesourcethat produced it (rule,llm, orprobe). The response'smonitor_disabled: truemeans no monitor router is configured — treat it as "no pathologies detected".
When you decide to act, circuit_break performs the reset atomically: it snapshots current working memory into a durable, run-scoped loop_detected lesson (tagged with your reason), clears working memory, resets the drift-monitor state, and emits a CIRCUIT_BROKEN event. The response returns broken, the snapshot_id of the snapshot entry, and a summary — so the pre-break state remains recoverable from long-term memory (see Circuit break: clear, but snapshot first).
Drift scoring needs a reference point. drift_score compares each query's embedding against the agent's declared objective; without one it reports -1 (unknown). Supply a client-computed objective_embedding when registering the agent to switch it on.
SDK surface: the run-signal op is client.advanced.get_run_signal({...}) in Python and client.advanced.getRunSignal({...}) in JS; the Rust SDK does not bind it at 0.11.0 — call POST /v2/control/run-monitor/signal directly. circuit_break is typed in all three languages from SDK v0.12.0 (client.circuit_break(reason=...) / client.circuitBreak({ reason }) / CircuitBreakOptions in Rust, which had it earlier); on 0.11.0 the Python/JS calls are top-level wire pass-throughs with the same call shape (wire field names: run_id, reason, agent_id; the client's ambient run ID is auto-injected when run_id is omitted), so the code below runs unchanged on either version. objective_embedding is a wire-level field on the agent-register request that none of the typed register_agent helpers forward — in JS use the raw passthrough client.advanced.registerAgent({ run_id, agent_id, objective_embedding: [...] }); in Python use client.advanced.register_agent({...}) from SDK v0.12.0 (earlier Python versions and Rust: send POST /v2/control/agents/register directly).
Walkthrough
Ari is stuck on a ticket, re-issuing the same failing tool call. The Nimbus orchestrator polls the run signal between steps, breaks the loop, and seeds a fresh attempt from durable memory:
The fresh attempt starts with clean working memory but a briefed context: long-term memory — facts, lessons, and now the tagged snapshot of the broken attempt — survives the break by design. If the reset itself turns out to be wrong, the snapshot_id points at the pre-break state.
To enable drift detection for Ari, register the agent with an objective embedding (computed with your own embedding model):
// JS raw passthrough — the typed register helpers don't forward this field.
await client.advanced.registerAgent({
run_id: runId,
agent_id: "ari",
role: "support",
objective_embedding: await embed("Resolve Nimbus support tickets end to end"),
});Wire the signals into decisions, not reflexes. is_looping from a low-confidence rule source on a single poll is a prompt to look, not to reset — an agent legitimately polling a slow job can look "repeated". Gate on confidence, require the flag to persist across polls, and record the outcome of every break (the outcome attribution loop) so your thresholds themselves improve.
When not to use it
| Situation | Do instead |
|---|---|
| Transient errors (timeouts, 429s, flaky dependency) | Plain retries with backoff — nothing is wrong with the agent's state |
| The failure has a recurring, learnable shape | Guardrails from failures — prevent it, don't just interrupt it |
| You want a deliberate pause point, not a reset | checkpoint() — snapshot without clearing (the bridge) |
monitor_disabled: true and you need loop detection | Read the per-recall signals block, or enable the run monitor on the instance (below) |
Related
- Working memory vs long-term memory — what circuit break clears, and what survives it
- Guardrails from failures — turning the diagnosed pathology into prevention
- The outcome attribution loop — crediting whether a break helped
- Runs, sessions, and scopes — the run boundary the snapshot lives in
- SDK helpers — when to drop to
client.advanced.*raw methods
Configuration. The run monitor is instance configuration: MUBIT_CONTROL_RUN_MONITOR_ENABLED turns the router on, and MUBIT_CONTROL_RUN_MONITOR_MODE selects rule (cheap heuristics, the default), llm (semantic loop/drift detection), or hybrid. The per-recall signals block works independently of the router.