ouroboros-ai 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""Stagnation detection for Ouroboros execution cycles.
|
|
2
|
+
|
|
3
|
+
This module implements Story 4.1: Stagnation Detection (4 Patterns).
|
|
4
|
+
|
|
5
|
+
Detects 4 stagnation patterns:
|
|
6
|
+
1. Spinning: Same output repeated (e.g., same error 3+ times)
|
|
7
|
+
2. Oscillation: A→B→A→B alternating pattern
|
|
8
|
+
3. No Drift: No progress toward goal (drift score unchanging)
|
|
9
|
+
4. Diminishing Returns: Progress rate decreasing
|
|
10
|
+
|
|
11
|
+
Design:
|
|
12
|
+
- Stateless detector: All state passed via ExecutionHistory
|
|
13
|
+
- Hash-based comparison: Fast, O(1) for most patterns
|
|
14
|
+
- Event emission: Each pattern emits its own event type
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from ouroboros.resilience.stagnation import (
|
|
18
|
+
StagnationDetector,
|
|
19
|
+
ExecutionHistory,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Build history from execution
|
|
23
|
+
history = ExecutionHistory(
|
|
24
|
+
phase_outputs=["output1", "output2", "output1", "output2"],
|
|
25
|
+
error_signatures=["error_A", "error_A"],
|
|
26
|
+
drift_scores=[0.5, 0.5, 0.5],
|
|
27
|
+
iteration=3,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Detect patterns
|
|
31
|
+
detector = StagnationDetector()
|
|
32
|
+
result = detector.detect(history)
|
|
33
|
+
|
|
34
|
+
for detection in result.value:
|
|
35
|
+
if detection.detected:
|
|
36
|
+
print(f"Stagnation: {detection.pattern.value}")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import hashlib
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from datetime import UTC, datetime
|
|
44
|
+
from enum import Enum
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
from ouroboros.core.types import Result
|
|
48
|
+
from ouroboros.events.base import BaseEvent
|
|
49
|
+
from ouroboros.observability.logging import get_logger
|
|
50
|
+
|
|
51
|
+
log = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Enums and Data Models
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StagnationPattern(str, Enum):
|
|
60
|
+
"""Four stagnation patterns detected in execution loops.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
SPINNING: Identical outputs repeated (same error, same result)
|
|
64
|
+
OSCILLATION: Alternating A→B→A→B pattern (flip-flopping)
|
|
65
|
+
NO_DRIFT: Output generated but no progress toward goal
|
|
66
|
+
DIMINISHING_RETURNS: Progress rate consistently decreasing
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
SPINNING = "spinning"
|
|
70
|
+
OSCILLATION = "oscillation"
|
|
71
|
+
NO_DRIFT = "no_drift"
|
|
72
|
+
DIMINISHING_RETURNS = "diminishing_returns"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def default_threshold(self) -> int:
|
|
76
|
+
"""Return default threshold for this pattern."""
|
|
77
|
+
thresholds = {
|
|
78
|
+
StagnationPattern.SPINNING: 3,
|
|
79
|
+
StagnationPattern.OSCILLATION: 2,
|
|
80
|
+
StagnationPattern.NO_DRIFT: 3,
|
|
81
|
+
StagnationPattern.DIMINISHING_RETURNS: 3,
|
|
82
|
+
}
|
|
83
|
+
return thresholds[self]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class StagnationDetection:
|
|
88
|
+
"""Result of stagnation pattern detection.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
pattern: The type of stagnation pattern checked.
|
|
92
|
+
detected: Whether stagnation was detected.
|
|
93
|
+
confidence: Confidence score (0.0-1.0).
|
|
94
|
+
evidence: Pattern-specific evidence supporting detection.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
pattern: StagnationPattern
|
|
98
|
+
detected: bool
|
|
99
|
+
confidence: float
|
|
100
|
+
evidence: dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class ExecutionHistory:
|
|
105
|
+
"""Historical execution data for stagnation detection.
|
|
106
|
+
|
|
107
|
+
Contains recent outputs and metrics needed for pattern analysis.
|
|
108
|
+
All collections are tuples to ensure immutability.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
phase_outputs: Recent phase outputs (strings, for hash comparison).
|
|
112
|
+
error_signatures: Recent error messages (for spinning detection).
|
|
113
|
+
drift_scores: Recent drift measurements (for no_drift/diminishing).
|
|
114
|
+
iteration: Current iteration number.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
phase_outputs: tuple[str, ...] = field(default_factory=tuple)
|
|
118
|
+
error_signatures: tuple[str, ...] = field(default_factory=tuple)
|
|
119
|
+
drift_scores: tuple[float, ...] = field(default_factory=tuple)
|
|
120
|
+
iteration: int = 0
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_lists(
|
|
124
|
+
cls,
|
|
125
|
+
phase_outputs: list[str],
|
|
126
|
+
error_signatures: list[str],
|
|
127
|
+
drift_scores: list[float],
|
|
128
|
+
iteration: int,
|
|
129
|
+
) -> ExecutionHistory:
|
|
130
|
+
"""Create ExecutionHistory from mutable lists.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
phase_outputs: Recent phase output strings.
|
|
134
|
+
error_signatures: Recent error messages.
|
|
135
|
+
drift_scores: Recent drift score values.
|
|
136
|
+
iteration: Current iteration number.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Immutable ExecutionHistory instance.
|
|
140
|
+
"""
|
|
141
|
+
return cls(
|
|
142
|
+
phase_outputs=tuple(phase_outputs),
|
|
143
|
+
error_signatures=tuple(error_signatures),
|
|
144
|
+
drift_scores=tuple(drift_scores),
|
|
145
|
+
iteration=iteration,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# =============================================================================
|
|
150
|
+
# Stagnation Detector
|
|
151
|
+
# =============================================================================
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StagnationDetector:
|
|
155
|
+
"""Stateless detector for stagnation patterns.
|
|
156
|
+
|
|
157
|
+
Analyzes ExecutionHistory to detect 4 stagnation patterns.
|
|
158
|
+
All detection methods are pure functions operating on history.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
spinning_threshold: Repetitions needed for spinning detection.
|
|
162
|
+
oscillation_cycles: Complete A→B cycles needed.
|
|
163
|
+
no_drift_epsilon: Maximum drift change to consider "no progress".
|
|
164
|
+
no_drift_iterations: Iterations with no drift to trigger detection.
|
|
165
|
+
diminishing_threshold: Improvement rate below this triggers detection.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
# Default thresholds (can be overridden via __init__)
|
|
169
|
+
DEFAULT_SPINNING_THRESHOLD = 3
|
|
170
|
+
DEFAULT_OSCILLATION_CYCLES = 2
|
|
171
|
+
DEFAULT_NO_DRIFT_EPSILON = 0.01
|
|
172
|
+
DEFAULT_NO_DRIFT_ITERATIONS = 3
|
|
173
|
+
DEFAULT_DIMINISHING_THRESHOLD = 0.01
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
spinning_threshold: int | None = None,
|
|
179
|
+
oscillation_cycles: int | None = None,
|
|
180
|
+
no_drift_epsilon: float | None = None,
|
|
181
|
+
no_drift_iterations: int | None = None,
|
|
182
|
+
diminishing_threshold: float | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Initialize StagnationDetector with configurable thresholds.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
spinning_threshold: Repetitions for spinning (default: 3).
|
|
188
|
+
oscillation_cycles: A→B cycles for oscillation (default: 2).
|
|
189
|
+
no_drift_epsilon: Max drift delta for no_drift (default: 0.01).
|
|
190
|
+
no_drift_iterations: Iterations for no_drift (default: 3).
|
|
191
|
+
diminishing_threshold: Min improvement rate (default: 0.01).
|
|
192
|
+
"""
|
|
193
|
+
self._spinning_threshold = spinning_threshold or self.DEFAULT_SPINNING_THRESHOLD
|
|
194
|
+
self._oscillation_cycles = oscillation_cycles or self.DEFAULT_OSCILLATION_CYCLES
|
|
195
|
+
self._no_drift_epsilon = no_drift_epsilon or self.DEFAULT_NO_DRIFT_EPSILON
|
|
196
|
+
self._no_drift_iterations = no_drift_iterations or self.DEFAULT_NO_DRIFT_ITERATIONS
|
|
197
|
+
self._diminishing_threshold = diminishing_threshold or self.DEFAULT_DIMINISHING_THRESHOLD
|
|
198
|
+
|
|
199
|
+
def _compute_hash(self, text: str) -> str:
|
|
200
|
+
"""Compute SHA-256 hash of text for fast comparison.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
text: Text to hash.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
First 16 characters of hex digest (enough for collision avoidance).
|
|
207
|
+
"""
|
|
208
|
+
return hashlib.sha256(text.encode()).hexdigest()[:16]
|
|
209
|
+
|
|
210
|
+
def detect(self, history: ExecutionHistory) -> Result[list[StagnationDetection], None]:
|
|
211
|
+
"""Detect all stagnation patterns from execution history.
|
|
212
|
+
|
|
213
|
+
Runs all 4 pattern detectors and returns results.
|
|
214
|
+
Each detector is independent and non-blocking.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
history: ExecutionHistory with recent outputs and metrics.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Result containing list of StagnationDetection for each pattern.
|
|
221
|
+
Always returns Ok (detection never fails, just detects or not).
|
|
222
|
+
"""
|
|
223
|
+
log.debug(
|
|
224
|
+
"resilience.stagnation.detection_started",
|
|
225
|
+
iteration=history.iteration,
|
|
226
|
+
outputs_count=len(history.phase_outputs),
|
|
227
|
+
errors_count=len(history.error_signatures),
|
|
228
|
+
drifts_count=len(history.drift_scores),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
detections = [
|
|
232
|
+
self._detect_spinning(history),
|
|
233
|
+
self._detect_oscillation(history),
|
|
234
|
+
self._detect_no_drift(history),
|
|
235
|
+
self._detect_diminishing_returns(history),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
detected_count = sum(1 for d in detections if d.detected)
|
|
239
|
+
if detected_count > 0:
|
|
240
|
+
log.info(
|
|
241
|
+
"resilience.stagnation.patterns_detected",
|
|
242
|
+
count=detected_count,
|
|
243
|
+
patterns=[d.pattern.value for d in detections if d.detected],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return Result.ok(detections)
|
|
247
|
+
|
|
248
|
+
def _detect_spinning(self, history: ExecutionHistory) -> StagnationDetection:
|
|
249
|
+
"""Detect spinning pattern: same output repeated N times.
|
|
250
|
+
|
|
251
|
+
Checks both phase_outputs and error_signatures for repetition.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
history: ExecutionHistory to analyze.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
StagnationDetection with detected=True if spinning found.
|
|
258
|
+
"""
|
|
259
|
+
# Check phase outputs
|
|
260
|
+
outputs = history.phase_outputs
|
|
261
|
+
if len(outputs) >= self._spinning_threshold:
|
|
262
|
+
recent = outputs[-self._spinning_threshold :]
|
|
263
|
+
hashes = [self._compute_hash(o) for o in recent]
|
|
264
|
+
|
|
265
|
+
if len(set(hashes)) == 1:
|
|
266
|
+
return StagnationDetection(
|
|
267
|
+
pattern=StagnationPattern.SPINNING,
|
|
268
|
+
detected=True,
|
|
269
|
+
confidence=1.0,
|
|
270
|
+
evidence={
|
|
271
|
+
"repeated_output_sample": recent[-1][:200],
|
|
272
|
+
"repeat_count": len(recent),
|
|
273
|
+
"source": "phase_outputs",
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Check error signatures
|
|
278
|
+
errors = history.error_signatures
|
|
279
|
+
if len(errors) >= self._spinning_threshold:
|
|
280
|
+
recent_errors = errors[-self._spinning_threshold :]
|
|
281
|
+
error_hashes = [self._compute_hash(e) for e in recent_errors]
|
|
282
|
+
|
|
283
|
+
if len(set(error_hashes)) == 1:
|
|
284
|
+
return StagnationDetection(
|
|
285
|
+
pattern=StagnationPattern.SPINNING,
|
|
286
|
+
detected=True,
|
|
287
|
+
confidence=1.0,
|
|
288
|
+
evidence={
|
|
289
|
+
"repeated_error": recent_errors[-1][:200],
|
|
290
|
+
"repeat_count": len(recent_errors),
|
|
291
|
+
"source": "error_signatures",
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return StagnationDetection(
|
|
296
|
+
pattern=StagnationPattern.SPINNING,
|
|
297
|
+
detected=False,
|
|
298
|
+
confidence=0.0,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _detect_oscillation(self, history: ExecutionHistory) -> StagnationDetection:
|
|
302
|
+
"""Detect oscillation pattern: A→B→A→B alternating states.
|
|
303
|
+
|
|
304
|
+
Looks for alternating pattern where even indices match each other
|
|
305
|
+
and odd indices match each other, but even ≠ odd.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
history: ExecutionHistory to analyze.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
StagnationDetection with detected=True if oscillation found.
|
|
312
|
+
"""
|
|
313
|
+
min_length = self._oscillation_cycles * 2
|
|
314
|
+
outputs = history.phase_outputs
|
|
315
|
+
|
|
316
|
+
if len(outputs) < min_length:
|
|
317
|
+
return StagnationDetection(
|
|
318
|
+
pattern=StagnationPattern.OSCILLATION,
|
|
319
|
+
detected=False,
|
|
320
|
+
confidence=0.0,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
recent = outputs[-min_length:]
|
|
324
|
+
hashes = [self._compute_hash(o) for o in recent]
|
|
325
|
+
|
|
326
|
+
# Split into even and odd indices
|
|
327
|
+
even_hashes = hashes[::2] # [0, 2, 4, ...]
|
|
328
|
+
odd_hashes = hashes[1::2] # [1, 3, 5, ...]
|
|
329
|
+
|
|
330
|
+
# Check: all evens same, all odds same, but even ≠ odd
|
|
331
|
+
evens_same = len(set(even_hashes)) == 1
|
|
332
|
+
odds_same = len(set(odd_hashes)) == 1
|
|
333
|
+
different_states = even_hashes[0] != odd_hashes[0]
|
|
334
|
+
|
|
335
|
+
if evens_same and odds_same and different_states:
|
|
336
|
+
return StagnationDetection(
|
|
337
|
+
pattern=StagnationPattern.OSCILLATION,
|
|
338
|
+
detected=True,
|
|
339
|
+
confidence=0.9,
|
|
340
|
+
evidence={
|
|
341
|
+
"state_a_sample": recent[0][:100],
|
|
342
|
+
"state_b_sample": recent[1][:100],
|
|
343
|
+
"cycles_detected": self._oscillation_cycles,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return StagnationDetection(
|
|
348
|
+
pattern=StagnationPattern.OSCILLATION,
|
|
349
|
+
detected=False,
|
|
350
|
+
confidence=0.0,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _detect_no_drift(self, history: ExecutionHistory) -> StagnationDetection:
|
|
354
|
+
"""Detect no drift pattern: drift score not improving.
|
|
355
|
+
|
|
356
|
+
Checks if drift scores have changed by less than epsilon
|
|
357
|
+
over the required number of iterations.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
history: ExecutionHistory with drift_scores.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
StagnationDetection with detected=True if no drift found.
|
|
364
|
+
"""
|
|
365
|
+
scores = history.drift_scores
|
|
366
|
+
|
|
367
|
+
if len(scores) < self._no_drift_iterations:
|
|
368
|
+
return StagnationDetection(
|
|
369
|
+
pattern=StagnationPattern.NO_DRIFT,
|
|
370
|
+
detected=False,
|
|
371
|
+
confidence=0.0,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
recent = scores[-self._no_drift_iterations :]
|
|
375
|
+
|
|
376
|
+
# Calculate deltas between consecutive scores
|
|
377
|
+
deltas = [abs(recent[i] - recent[i - 1]) for i in range(1, len(recent))]
|
|
378
|
+
|
|
379
|
+
# Check if all deltas are below epsilon
|
|
380
|
+
if all(delta < self._no_drift_epsilon for delta in deltas):
|
|
381
|
+
avg_score = sum(recent) / len(recent)
|
|
382
|
+
return StagnationDetection(
|
|
383
|
+
pattern=StagnationPattern.NO_DRIFT,
|
|
384
|
+
detected=True,
|
|
385
|
+
confidence=1.0 - (sum(deltas) / len(deltas) / self._no_drift_epsilon),
|
|
386
|
+
evidence={
|
|
387
|
+
"drift_scores": list(recent),
|
|
388
|
+
"deltas": deltas,
|
|
389
|
+
"epsilon_threshold": self._no_drift_epsilon,
|
|
390
|
+
"average_drift": avg_score,
|
|
391
|
+
"stagnant_iterations": len(recent),
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return StagnationDetection(
|
|
396
|
+
pattern=StagnationPattern.NO_DRIFT,
|
|
397
|
+
detected=False,
|
|
398
|
+
confidence=0.0,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _detect_diminishing_returns(self, history: ExecutionHistory) -> StagnationDetection:
|
|
402
|
+
"""Detect diminishing returns: improvement rate decreasing.
|
|
403
|
+
|
|
404
|
+
Analyzes drift scores to check if improvements are getting smaller.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
history: ExecutionHistory with drift_scores.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
StagnationDetection with detected=True if diminishing returns found.
|
|
411
|
+
"""
|
|
412
|
+
scores = history.drift_scores
|
|
413
|
+
|
|
414
|
+
if len(scores) < self._no_drift_iterations + 1:
|
|
415
|
+
return StagnationDetection(
|
|
416
|
+
pattern=StagnationPattern.DIMINISHING_RETURNS,
|
|
417
|
+
detected=False,
|
|
418
|
+
confidence=0.0,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
recent = scores[-(self._no_drift_iterations + 1) :]
|
|
422
|
+
|
|
423
|
+
# Calculate improvement rates (positive = improving toward goal)
|
|
424
|
+
# Assuming lower drift = better (closer to goal)
|
|
425
|
+
improvements = [recent[i - 1] - recent[i] for i in range(1, len(recent))]
|
|
426
|
+
|
|
427
|
+
# Check if all improvements are below threshold
|
|
428
|
+
if all(imp < self._diminishing_threshold for imp in improvements):
|
|
429
|
+
# Additional check: are improvements decreasing?
|
|
430
|
+
is_decreasing = all(
|
|
431
|
+
improvements[i] >= improvements[i + 1] for i in range(len(improvements) - 1)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
confidence = 0.8 if is_decreasing else 0.6
|
|
435
|
+
|
|
436
|
+
return StagnationDetection(
|
|
437
|
+
pattern=StagnationPattern.DIMINISHING_RETURNS,
|
|
438
|
+
detected=True,
|
|
439
|
+
confidence=confidence,
|
|
440
|
+
evidence={
|
|
441
|
+
"improvement_rates": improvements,
|
|
442
|
+
"threshold": self._diminishing_threshold,
|
|
443
|
+
"monotonically_decreasing": is_decreasing,
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return StagnationDetection(
|
|
448
|
+
pattern=StagnationPattern.DIMINISHING_RETURNS,
|
|
449
|
+
detected=False,
|
|
450
|
+
confidence=0.0,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# =============================================================================
|
|
455
|
+
# Event Classes
|
|
456
|
+
# =============================================================================
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class SpinningDetectedEvent(BaseEvent):
|
|
460
|
+
"""Event emitted when spinning pattern detected.
|
|
461
|
+
|
|
462
|
+
Spinning occurs when the same output is repeated multiple times,
|
|
463
|
+
indicating the system is stuck in a loop.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
def __init__(
|
|
467
|
+
self,
|
|
468
|
+
execution_id: str,
|
|
469
|
+
repeated_output_sample: str,
|
|
470
|
+
repeat_count: int,
|
|
471
|
+
source: str,
|
|
472
|
+
*,
|
|
473
|
+
seed_id: str | None = None,
|
|
474
|
+
confidence: float = 1.0,
|
|
475
|
+
iteration: int = 0,
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Create SpinningDetectedEvent.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
execution_id: Execution identifier.
|
|
481
|
+
repeated_output_sample: Sample of repeated output (truncated).
|
|
482
|
+
repeat_count: Number of repetitions detected.
|
|
483
|
+
source: Where repetition was found ("phase_outputs" or "error_signatures").
|
|
484
|
+
seed_id: Optional seed identifier.
|
|
485
|
+
confidence: Confidence score (0.0-1.0).
|
|
486
|
+
iteration: Current iteration number.
|
|
487
|
+
"""
|
|
488
|
+
super().__init__(
|
|
489
|
+
type="resilience.stagnation.spinning.detected",
|
|
490
|
+
aggregate_type="execution",
|
|
491
|
+
aggregate_id=execution_id,
|
|
492
|
+
data={
|
|
493
|
+
"pattern": StagnationPattern.SPINNING.value,
|
|
494
|
+
"repeated_output_sample": repeated_output_sample[:200],
|
|
495
|
+
"repeat_count": repeat_count,
|
|
496
|
+
"source": source,
|
|
497
|
+
"seed_id": seed_id,
|
|
498
|
+
"confidence": confidence,
|
|
499
|
+
"iteration": iteration,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class OscillationDetectedEvent(BaseEvent):
|
|
505
|
+
"""Event emitted when oscillation pattern detected.
|
|
506
|
+
|
|
507
|
+
Oscillation occurs when execution alternates between two states
|
|
508
|
+
in an A→B→A→B pattern.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
def __init__(
|
|
512
|
+
self,
|
|
513
|
+
execution_id: str,
|
|
514
|
+
state_a_sample: str,
|
|
515
|
+
state_b_sample: str,
|
|
516
|
+
cycles_detected: int,
|
|
517
|
+
*,
|
|
518
|
+
seed_id: str | None = None,
|
|
519
|
+
confidence: float = 0.9,
|
|
520
|
+
iteration: int = 0,
|
|
521
|
+
) -> None:
|
|
522
|
+
"""Create OscillationDetectedEvent.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
execution_id: Execution identifier.
|
|
526
|
+
state_a_sample: Sample of state A (truncated).
|
|
527
|
+
state_b_sample: Sample of state B (truncated).
|
|
528
|
+
cycles_detected: Number of A→B cycles detected.
|
|
529
|
+
seed_id: Optional seed identifier.
|
|
530
|
+
confidence: Confidence score (0.0-1.0).
|
|
531
|
+
iteration: Current iteration number.
|
|
532
|
+
"""
|
|
533
|
+
super().__init__(
|
|
534
|
+
type="resilience.stagnation.oscillation.detected",
|
|
535
|
+
aggregate_type="execution",
|
|
536
|
+
aggregate_id=execution_id,
|
|
537
|
+
data={
|
|
538
|
+
"pattern": StagnationPattern.OSCILLATION.value,
|
|
539
|
+
"state_a_sample": state_a_sample[:100],
|
|
540
|
+
"state_b_sample": state_b_sample[:100],
|
|
541
|
+
"cycles_detected": cycles_detected,
|
|
542
|
+
"seed_id": seed_id,
|
|
543
|
+
"confidence": confidence,
|
|
544
|
+
"iteration": iteration,
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
class NoDriftDetectedEvent(BaseEvent):
|
|
550
|
+
"""Event emitted when no drift pattern detected.
|
|
551
|
+
|
|
552
|
+
No drift occurs when the drift score (distance from goal) is not
|
|
553
|
+
improving over multiple iterations.
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
def __init__(
|
|
557
|
+
self,
|
|
558
|
+
execution_id: str,
|
|
559
|
+
drift_scores: list[float],
|
|
560
|
+
average_drift: float,
|
|
561
|
+
stagnant_iterations: int,
|
|
562
|
+
*,
|
|
563
|
+
seed_id: str | None = None,
|
|
564
|
+
confidence: float = 1.0,
|
|
565
|
+
iteration: int = 0,
|
|
566
|
+
) -> None:
|
|
567
|
+
"""Create NoDriftDetectedEvent.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
execution_id: Execution identifier.
|
|
571
|
+
drift_scores: Recent drift score values.
|
|
572
|
+
average_drift: Average drift score over period.
|
|
573
|
+
stagnant_iterations: Number of iterations with no progress.
|
|
574
|
+
seed_id: Optional seed identifier.
|
|
575
|
+
confidence: Confidence score (0.0-1.0).
|
|
576
|
+
iteration: Current iteration number.
|
|
577
|
+
"""
|
|
578
|
+
super().__init__(
|
|
579
|
+
type="resilience.stagnation.no_drift.detected",
|
|
580
|
+
aggregate_type="execution",
|
|
581
|
+
aggregate_id=execution_id,
|
|
582
|
+
data={
|
|
583
|
+
"pattern": StagnationPattern.NO_DRIFT.value,
|
|
584
|
+
"drift_scores": drift_scores,
|
|
585
|
+
"average_drift": average_drift,
|
|
586
|
+
"stagnant_iterations": stagnant_iterations,
|
|
587
|
+
"seed_id": seed_id,
|
|
588
|
+
"confidence": confidence,
|
|
589
|
+
"iteration": iteration,
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class DiminishingReturnsDetectedEvent(BaseEvent):
|
|
595
|
+
"""Event emitted when diminishing returns pattern detected.
|
|
596
|
+
|
|
597
|
+
Diminishing returns occurs when the improvement rate is consistently
|
|
598
|
+
decreasing, indicating progress is slowing.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
def __init__(
|
|
602
|
+
self,
|
|
603
|
+
execution_id: str,
|
|
604
|
+
improvement_rates: list[float],
|
|
605
|
+
monotonically_decreasing: bool,
|
|
606
|
+
*,
|
|
607
|
+
seed_id: str | None = None,
|
|
608
|
+
confidence: float = 0.8,
|
|
609
|
+
iteration: int = 0,
|
|
610
|
+
) -> None:
|
|
611
|
+
"""Create DiminishingReturnsDetectedEvent.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
execution_id: Execution identifier.
|
|
615
|
+
improvement_rates: Recent improvement rate values.
|
|
616
|
+
monotonically_decreasing: Whether rates are strictly decreasing.
|
|
617
|
+
seed_id: Optional seed identifier.
|
|
618
|
+
confidence: Confidence score (0.0-1.0).
|
|
619
|
+
iteration: Current iteration number.
|
|
620
|
+
"""
|
|
621
|
+
super().__init__(
|
|
622
|
+
type="resilience.stagnation.diminishing_returns.detected",
|
|
623
|
+
aggregate_type="execution",
|
|
624
|
+
aggregate_id=execution_id,
|
|
625
|
+
data={
|
|
626
|
+
"pattern": StagnationPattern.DIMINISHING_RETURNS.value,
|
|
627
|
+
"improvement_rates": improvement_rates,
|
|
628
|
+
"monotonically_decreasing": monotonically_decreasing,
|
|
629
|
+
"seed_id": seed_id,
|
|
630
|
+
"confidence": confidence,
|
|
631
|
+
"iteration": iteration,
|
|
632
|
+
},
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# =============================================================================
|
|
637
|
+
# Helper Functions
|
|
638
|
+
# =============================================================================
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def create_stagnation_event(
|
|
642
|
+
detection: StagnationDetection,
|
|
643
|
+
execution_id: str,
|
|
644
|
+
*,
|
|
645
|
+
seed_id: str | None = None,
|
|
646
|
+
iteration: int = 0,
|
|
647
|
+
) -> BaseEvent:
|
|
648
|
+
"""Create appropriate event for a stagnation detection.
|
|
649
|
+
|
|
650
|
+
Factory function that creates the correct event type based on
|
|
651
|
+
the detected pattern.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
detection: StagnationDetection result.
|
|
655
|
+
execution_id: Execution identifier.
|
|
656
|
+
seed_id: Optional seed identifier.
|
|
657
|
+
iteration: Current iteration number.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Appropriate event type for the detected pattern.
|
|
661
|
+
|
|
662
|
+
Raises:
|
|
663
|
+
ValueError: If detection.detected is False.
|
|
664
|
+
"""
|
|
665
|
+
if not detection.detected:
|
|
666
|
+
raise ValueError("Cannot create event for non-detected stagnation")
|
|
667
|
+
|
|
668
|
+
evidence = detection.evidence
|
|
669
|
+
|
|
670
|
+
match detection.pattern:
|
|
671
|
+
case StagnationPattern.SPINNING:
|
|
672
|
+
return SpinningDetectedEvent(
|
|
673
|
+
execution_id=execution_id,
|
|
674
|
+
repeated_output_sample=str(evidence.get("repeated_output_sample", evidence.get("repeated_error", ""))),
|
|
675
|
+
repeat_count=int(evidence.get("repeat_count", 0)),
|
|
676
|
+
source=str(evidence.get("source", "unknown")),
|
|
677
|
+
seed_id=seed_id,
|
|
678
|
+
confidence=detection.confidence,
|
|
679
|
+
iteration=iteration,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
case StagnationPattern.OSCILLATION:
|
|
683
|
+
return OscillationDetectedEvent(
|
|
684
|
+
execution_id=execution_id,
|
|
685
|
+
state_a_sample=str(evidence.get("state_a_sample", "")),
|
|
686
|
+
state_b_sample=str(evidence.get("state_b_sample", "")),
|
|
687
|
+
cycles_detected=int(evidence.get("cycles_detected", 0)),
|
|
688
|
+
seed_id=seed_id,
|
|
689
|
+
confidence=detection.confidence,
|
|
690
|
+
iteration=iteration,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
case StagnationPattern.NO_DRIFT:
|
|
694
|
+
return NoDriftDetectedEvent(
|
|
695
|
+
execution_id=execution_id,
|
|
696
|
+
drift_scores=list(evidence.get("drift_scores", [])),
|
|
697
|
+
average_drift=float(evidence.get("average_drift", 0.0)),
|
|
698
|
+
stagnant_iterations=int(evidence.get("stagnant_iterations", 0)),
|
|
699
|
+
seed_id=seed_id,
|
|
700
|
+
confidence=detection.confidence,
|
|
701
|
+
iteration=iteration,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
case StagnationPattern.DIMINISHING_RETURNS:
|
|
705
|
+
return DiminishingReturnsDetectedEvent(
|
|
706
|
+
execution_id=execution_id,
|
|
707
|
+
improvement_rates=list(evidence.get("improvement_rates", [])),
|
|
708
|
+
monotonically_decreasing=bool(evidence.get("monotonically_decreasing", False)),
|
|
709
|
+
seed_id=seed_id,
|
|
710
|
+
confidence=detection.confidence,
|
|
711
|
+
iteration=iteration,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
case _:
|
|
715
|
+
# Fallback - should never reach here
|
|
716
|
+
return BaseEvent(
|
|
717
|
+
type=f"resilience.stagnation.{detection.pattern.value}.detected",
|
|
718
|
+
aggregate_type="execution",
|
|
719
|
+
aggregate_id=execution_id,
|
|
720
|
+
data={
|
|
721
|
+
"pattern": detection.pattern.value,
|
|
722
|
+
"evidence": evidence,
|
|
723
|
+
"confidence": detection.confidence,
|
|
724
|
+
"seed_id": seed_id,
|
|
725
|
+
"iteration": iteration,
|
|
726
|
+
},
|
|
727
|
+
)
|