Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content
Learning from outcomes

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 lightweight signals block: 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, plus loop_score, drift_score, a human-readable rationale, a confidence, and the source that produced it (rule, llm, or probe). The response's monitor_disabled: true means 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.

ℹ️Note

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:

run_id = "nimbus:ari:ticket-2214"
 
# Supervisor loop: poll between agent steps.
resp = client.advanced.get_run_signal({"run_id": run_id, "agent_id": "ari"})
if not resp.get("monitor_disabled"):
    signal = resp.get("signal") or {}
    if signal.get("is_looping") and signal.get("confidence", 0.0) >= 0.7:
        print(signal["rationale"])   # "same export-status call repeated 6x with identical args"
 
        # Break the loop: snapshot → clear → reset, in one call.
        result = client.circuit_break(run_id=run_id, reason="repeated_tool_call")
        snapshot_id = result["snapshot_id"]
 
        # Seed a fresh attempt. The snapshot lesson is durable and run-scoped,
        # so the rebuilt briefing already includes what the broken attempt knew.
        briefing = client.get_context(
            session_id=run_id,
            query="Resume ticket 2214: export job stuck in 'queued', retries exhausted",
            max_token_budget=1500,
        )
        restart_agent(system_extra=briefing["context_block"])

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"),
});
⚠️Warning

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

SituationDo 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 shapeGuardrails from failures — prevent it, don't just interrupt it
You want a deliberate pause point, not a resetcheckpoint() — snapshot without clearing (the bridge)
monitor_disabled: true and you need loop detectionRead the per-recall signals block, or enable the run monitor on the instance (below)

Related

ℹ️Note

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.