How Stagnation Detection in Ouroboros Identifies Spinning, Oscillation, and No-Drift Patterns
Stagnation Detection in Ouroboros identifies non-productive execution loops by analyzing SHA‑256 hashed outputs for repetition, alternating state sequences for oscillation, and minimal drift-score deltas for stagnation, using a stateless detector that processes an ExecutionHistory object.
The Ouroboros framework employs a specialized StagnationDetector class to identify when AI agent execution enters non-productive loops. Implemented in src/ouroboros/resilience/stagnation.py, this stateless component analyzes recent execution history to flag three distinct failure patterns: spinning, oscillation, and no-drift conditions.
How the StagnationDetector Works
The StagnationDetector class operates as a pure function that accepts an ExecutionHistory object containing recent phase outputs, error signatures, and drift scores. It computes SHA‑256 hashes of discrete outputs to enable efficient pattern matching while avoiding storage of large text fragments. The detector invokes three specialized internal methods—_detect_spinning, _detect_oscillation, and _detect_no_drift—each returning a StagnationDetection object when their respective threshold conditions are met.
Pattern Detection Algorithms
Detecting Spinning Patterns
Spinning occurs when the same output or error repeats consecutively without progress. The _detect_spinning method (lines 247‑274) implements this check by:
- Retrieving the last N outputs from
history.phase_outputs, where N equals thespinning_threshold(default 3). - Computing a short SHA‑256 hash for each entry using
_compute_hash. - Returning a
StagnationDetectionwithpattern = SPINNINGif all hashes match. - If output hashing fails, repeating the process against
history.error_signatures.
When detected, the evidence includes the repeated_output_sample, repeat_count, and source field indicating whether the repetition originated from outputs or errors.
Detecting Oscillation Patterns
Oscillation represents an alternating A→B→A→B state flip that prevents convergence. The _detect_oscillation method (lines 300‑345) identifies this by:
- Validating that at least
oscillation_cycles× 2 items exist in recent outputs (default 2 cycles, requiring 4 items). - Splitting the hashed outputs into even-indexed (0, 2, …) and odd-indexed (1, 3, …) groups.
- Confirming uniform hashes within each group and inequality between groups.
- Returning
pattern = OSCILLATIONwhen the alternating condition holds.
This detection catches flip-flopping behaviors where the system alternates between two distinct states without making forward progress.
Detecting No-Drift Patterns
No-drift stagnation occurs when semantic drift scores remain nearly constant, indicating the agent is not evolving its understanding. The _detect_no_drift method (lines 352‑383) monitors history.drift_scores by:
- Examining the last N scores, where N equals
no_drift_iterations(default 3). - Computing absolute deltas between successive scores.
- Returning
pattern = NO_DRIFTif every delta falls belowno_drift_epsilon(default 0.01).
Confidence scoring derives from proximity to the epsilon threshold using the formula 1 - average_delta/epsilon, providing a normalized certainty metric for downstream recovery decisions.
Implementation Examples
Example 1 – Detecting Spinning
from ouroboros.resilience.stagnation import StagnationDetector, ExecutionHistory
history = ExecutionHistory.from_lists(
phase_outputs=["error A", "error A", "error A"], # three identical outputs
error_signatures=[],
drift_scores=[0.5, 0.5],
iteration=3,
)
detector = StagnationDetector()
result = detector.detect(history)
for detection in result.value:
if detection.pattern.name == "SPINNING" and detection.detected:
print("Spinning detected!", detection.evidence)
Output:
Spinning detected! {'repeated_output_sample': 'error A', 'repeat_count': 3, 'source': 'phase_outputs'}
Example 2 – Detecting Oscillation
history = ExecutionHistory.from_lists(
phase_outputs=["state A", "state B", "state A", "state B"],
error_signatures=[],
drift_scores=[0.45, 0.44, 0.45, 0.44],
iteration=4,
)
detector = StagnationDetector(oscillation_cycles=2) # default is 2 cycles
result = detector.detect(history)
osc = next(d for d in result.value if d.pattern == StagnationPattern.OSCILLATION)
if osc.detected:
print("Oscillation found:", osc.evidence)
Example 3 – Detecting No-Drift
history = ExecutionHistory.from_lists(
phase_outputs=["out1", "out2"],
error_signatures=[],
drift_scores=[0.30, 0.301, 0.302], # changes < 0.01
iteration=3,
)
detector = StagnationDetector(no_drift_epsilon=0.01, no_drift_iterations=3)
result = detector.detect(history)
no_drift = next(d for d in result.value if d.pattern == StagnationPattern.NO_DRIFT)
if no_drift.detected:
print("No drift:", no_drift.evidence)
Summary
- Stagnation Detection in Ouroboros uses a stateless
StagnationDetectorclass that analyzesExecutionHistoryobjects to identify three non-productive execution patterns. - Spinning detection relies on SHA‑256 hashing to identify identical outputs or errors repeating for at least three consecutive iterations (default threshold).
- Oscillation detection splits hashed outputs into even and odd indices to verify A→B→A→B alternating patterns over four or more items (default 2 cycles).
- No-drift detection monitors semantic drift-score deltas, flagging stagnation when consecutive changes remain below 0.01 for three or more iterations.
- All patterns return structured
StagnationDetectionobjects containing boolean flags, evidence dictionaries, and confidence scores suitable for automated recovery workflows.
Frequently Asked Questions
What is the default spinning threshold in Ouroboros stagnation detection?
The default spinning_threshold is 3, meaning the detector flags a spinning pattern when the same output or error signature repeats for three consecutive iterations. This value is configurable via the StagnationDetector constructor.
How does the oscillation detector distinguish between normal variation and harmful oscillation?
The detector specifically looks for alternating state patterns by comparing SHA‑256 hashes of even-indexed versus odd-indexed outputs in the recent history. It only flags OSCILLATION when the even group shares one identical hash, the odd group shares a different identical hash, and the pattern spans at least two full cycles (four items by default).
Can the no-drift detection sensitivity be adjusted for different agent behaviors?
Yes, the sensitivity is controlled by two parameters: no_drift_epsilon (default 0.01) sets the maximum allowed delta between consecutive drift scores, and no_drift_iterations (default 3) sets the required number of consecutive low-delta measurements. Increasing the epsilon makes the detector less sensitive to small fluctuations.
Where is the stagnation detection logic implemented in the Ouroboros codebase?
The core logic resides in src/ouroboros/resilience/stagnation.py, which contains the StagnationDetector class and the three private detection methods: _detect_spinning (lines 247‑274), _detect_oscillation (lines 300‑345), and _detect_no_drift (lines 352‑383).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →