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,383 @@
|
|
|
1
|
+
"""Drift measurement engine for Ouroboros.
|
|
2
|
+
|
|
3
|
+
This module implements Story 6.1: Drift Measurement Engine.
|
|
4
|
+
|
|
5
|
+
Drift measures how far the current execution state has deviated from
|
|
6
|
+
the original Seed specification. Three components are tracked:
|
|
7
|
+
1. Goal drift: Deviation from the stated objective
|
|
8
|
+
2. Constraint drift: Constraint violations accumulated
|
|
9
|
+
3. Ontology drift: Evolution of the concept space
|
|
10
|
+
|
|
11
|
+
The combined drift uses weighted formula (PRD 13.1):
|
|
12
|
+
combined = (goal * 0.5) + (constraint * 0.3) + (ontology * 0.2)
|
|
13
|
+
|
|
14
|
+
NFR5 requires combined drift ≤ 0.3 to be acceptable.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from ouroboros.observability.drift import (
|
|
18
|
+
DriftMeasurement,
|
|
19
|
+
DriftMetrics,
|
|
20
|
+
DriftMeasuredEvent,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
measurement = DriftMeasurement()
|
|
24
|
+
metrics = measurement.measure(
|
|
25
|
+
current_output="CLI task manager",
|
|
26
|
+
constraint_violations=[],
|
|
27
|
+
current_concepts=["tasks", "status"],
|
|
28
|
+
seed=seed,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if not metrics.is_acceptable:
|
|
32
|
+
# Combined drift > 0.3 - may need consensus
|
|
33
|
+
event = DriftThresholdExceededEvent(...)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
|
|
40
|
+
from ouroboros.core.seed import Seed
|
|
41
|
+
from ouroboros.events.base import BaseEvent
|
|
42
|
+
from ouroboros.observability.logging import get_logger
|
|
43
|
+
|
|
44
|
+
log = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Constants
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
# Weights from PRD 13.1
|
|
51
|
+
GOAL_DRIFT_WEIGHT = 0.5
|
|
52
|
+
CONSTRAINT_DRIFT_WEIGHT = 0.3
|
|
53
|
+
ONTOLOGY_DRIFT_WEIGHT = 0.2
|
|
54
|
+
|
|
55
|
+
# NFR5: Acceptable drift threshold
|
|
56
|
+
DRIFT_THRESHOLD = 0.3
|
|
57
|
+
|
|
58
|
+
# Constraint violation penalty per violation
|
|
59
|
+
CONSTRAINT_VIOLATION_PENALTY = 0.1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Data Models
|
|
64
|
+
# =============================================================================
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class DriftMetrics:
|
|
69
|
+
"""Immutable drift measurement result.
|
|
70
|
+
|
|
71
|
+
Contains individual drift components and computed combined drift.
|
|
72
|
+
Uses weighted formula from PRD 13.1.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
goal_drift: Deviation from seed goal (0.0-1.0)
|
|
76
|
+
constraint_drift: Constraint violations score (0.0-1.0)
|
|
77
|
+
ontology_drift: Concept space deviation (0.0-1.0)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
goal_drift: float
|
|
81
|
+
constraint_drift: float
|
|
82
|
+
ontology_drift: float
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
"""Validate value ranges."""
|
|
86
|
+
for attr in ("goal_drift", "constraint_drift", "ontology_drift"):
|
|
87
|
+
value = getattr(self, attr)
|
|
88
|
+
if not 0.0 <= value <= 1.0:
|
|
89
|
+
msg = f"{attr} must be between 0.0 and 1.0, got {value}"
|
|
90
|
+
raise ValueError(msg)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def combined_drift(self) -> float:
|
|
94
|
+
"""Calculate combined drift using weighted formula.
|
|
95
|
+
|
|
96
|
+
Formula from PRD 13.1:
|
|
97
|
+
combined = (goal * 0.5) + (constraint * 0.3) + (ontology * 0.2)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Combined drift score (0.0-1.0)
|
|
101
|
+
"""
|
|
102
|
+
return (
|
|
103
|
+
self.goal_drift * GOAL_DRIFT_WEIGHT
|
|
104
|
+
+ self.constraint_drift * CONSTRAINT_DRIFT_WEIGHT
|
|
105
|
+
+ self.ontology_drift * ONTOLOGY_DRIFT_WEIGHT
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_acceptable(self) -> bool:
|
|
110
|
+
"""Check if drift is within acceptable threshold.
|
|
111
|
+
|
|
112
|
+
NFR5 requires combined drift ≤ 0.3.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if combined drift ≤ 0.3
|
|
116
|
+
"""
|
|
117
|
+
return self.combined_drift <= DRIFT_THRESHOLD
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# =============================================================================
|
|
121
|
+
# Drift Calculation Functions
|
|
122
|
+
# =============================================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def calculate_goal_drift(current_output: str, seed: Seed) -> float:
|
|
126
|
+
"""Calculate goal drift using text similarity.
|
|
127
|
+
|
|
128
|
+
Measures how far the current output has drifted from the seed goal.
|
|
129
|
+
Uses Jaccard similarity of word sets as a simple baseline.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
current_output: Current execution output text
|
|
133
|
+
seed: Original seed specification
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Goal drift score (0.0 = identical, 1.0 = completely different)
|
|
137
|
+
"""
|
|
138
|
+
if not current_output or not current_output.strip():
|
|
139
|
+
return 1.0
|
|
140
|
+
|
|
141
|
+
# Tokenize and normalize
|
|
142
|
+
goal_words = _tokenize(seed.goal)
|
|
143
|
+
output_words = _tokenize(current_output)
|
|
144
|
+
|
|
145
|
+
if not goal_words:
|
|
146
|
+
return 1.0
|
|
147
|
+
|
|
148
|
+
# Jaccard similarity: |intersection| / |union|
|
|
149
|
+
intersection = goal_words & output_words
|
|
150
|
+
union = goal_words | output_words
|
|
151
|
+
|
|
152
|
+
if not union:
|
|
153
|
+
return 1.0
|
|
154
|
+
|
|
155
|
+
similarity = len(intersection) / len(union)
|
|
156
|
+
return 1.0 - similarity
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def calculate_constraint_drift(
|
|
160
|
+
constraint_violations: list[str], seed: Seed
|
|
161
|
+
) -> float:
|
|
162
|
+
"""Calculate constraint drift based on violations.
|
|
163
|
+
|
|
164
|
+
Each violation adds 0.1 to drift, capped at 1.0.
|
|
165
|
+
Formula from PRD 13.1: min(violations * 0.1, 1.0)
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
constraint_violations: List of violation descriptions
|
|
169
|
+
seed: Original seed specification (for context)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Constraint drift score (0.0-1.0)
|
|
173
|
+
"""
|
|
174
|
+
if not constraint_violations:
|
|
175
|
+
return 0.0
|
|
176
|
+
|
|
177
|
+
drift = len(constraint_violations) * CONSTRAINT_VIOLATION_PENALTY
|
|
178
|
+
return min(drift, 1.0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def calculate_ontology_drift(
|
|
182
|
+
current_concepts: list[str], seed: Seed
|
|
183
|
+
) -> float:
|
|
184
|
+
"""Calculate ontology drift based on concept evolution.
|
|
185
|
+
|
|
186
|
+
Measures how much the current concept space has drifted from
|
|
187
|
+
the original ontology schema defined in the seed.
|
|
188
|
+
|
|
189
|
+
Uses Jaccard distance between concept sets.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
current_concepts: Current list of concepts in use
|
|
193
|
+
seed: Original seed with ontology schema
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Ontology drift score (0.0-1.0)
|
|
197
|
+
"""
|
|
198
|
+
if not current_concepts:
|
|
199
|
+
return 1.0
|
|
200
|
+
|
|
201
|
+
# Extract seed ontology field names
|
|
202
|
+
seed_concepts = {
|
|
203
|
+
field.name.lower() for field in seed.ontology_schema.fields
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if not seed_concepts:
|
|
207
|
+
# No seed ontology defined - any concepts are acceptable
|
|
208
|
+
return 0.0
|
|
209
|
+
|
|
210
|
+
current_set = {c.lower() for c in current_concepts}
|
|
211
|
+
|
|
212
|
+
# Jaccard distance: 1 - (|intersection| / |union|)
|
|
213
|
+
intersection = seed_concepts & current_set
|
|
214
|
+
union = seed_concepts | current_set
|
|
215
|
+
|
|
216
|
+
if not union:
|
|
217
|
+
return 0.0
|
|
218
|
+
|
|
219
|
+
similarity = len(intersection) / len(union)
|
|
220
|
+
return 1.0 - similarity
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _tokenize(text: str) -> set[str]:
|
|
224
|
+
"""Tokenize text into normalized word set.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
text: Input text
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Set of lowercase words (alphanumeric only)
|
|
231
|
+
"""
|
|
232
|
+
# Simple word tokenization
|
|
233
|
+
words = text.lower().split()
|
|
234
|
+
# Keep only alphanumeric characters
|
|
235
|
+
return {
|
|
236
|
+
"".join(c for c in word if c.isalnum())
|
|
237
|
+
for word in words
|
|
238
|
+
if word
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# =============================================================================
|
|
243
|
+
# Drift Measurement Service
|
|
244
|
+
# =============================================================================
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class DriftMeasurement:
|
|
248
|
+
"""Service for measuring drift from seed.
|
|
249
|
+
|
|
250
|
+
Combines all three drift components into a unified measurement.
|
|
251
|
+
Stateless - all state passed via parameters.
|
|
252
|
+
|
|
253
|
+
Usage:
|
|
254
|
+
measurement = DriftMeasurement()
|
|
255
|
+
metrics = measurement.measure(
|
|
256
|
+
current_output="...",
|
|
257
|
+
constraint_violations=[...],
|
|
258
|
+
current_concepts=[...],
|
|
259
|
+
seed=seed,
|
|
260
|
+
)
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def measure(
|
|
264
|
+
self,
|
|
265
|
+
current_output: str,
|
|
266
|
+
constraint_violations: list[str],
|
|
267
|
+
current_concepts: list[str],
|
|
268
|
+
seed: Seed,
|
|
269
|
+
) -> DriftMetrics:
|
|
270
|
+
"""Measure all drift components.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
current_output: Current execution output
|
|
274
|
+
constraint_violations: List of constraint violations
|
|
275
|
+
current_concepts: Current ontology concepts
|
|
276
|
+
seed: Original seed specification
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
DriftMetrics with all components
|
|
280
|
+
"""
|
|
281
|
+
goal_drift = calculate_goal_drift(current_output, seed)
|
|
282
|
+
constraint_drift = calculate_constraint_drift(constraint_violations, seed)
|
|
283
|
+
ontology_drift = calculate_ontology_drift(current_concepts, seed)
|
|
284
|
+
|
|
285
|
+
metrics = DriftMetrics(
|
|
286
|
+
goal_drift=goal_drift,
|
|
287
|
+
constraint_drift=constraint_drift,
|
|
288
|
+
ontology_drift=ontology_drift,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
log.debug(
|
|
292
|
+
"observability.drift.measured",
|
|
293
|
+
seed_id=seed.metadata.seed_id,
|
|
294
|
+
goal_drift=goal_drift,
|
|
295
|
+
constraint_drift=constraint_drift,
|
|
296
|
+
ontology_drift=ontology_drift,
|
|
297
|
+
combined_drift=metrics.combined_drift,
|
|
298
|
+
is_acceptable=metrics.is_acceptable,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return metrics
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# =============================================================================
|
|
305
|
+
# Event Definitions
|
|
306
|
+
# =============================================================================
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class DriftMeasuredEvent(BaseEvent):
|
|
310
|
+
"""Event emitted when drift is measured after an iteration.
|
|
311
|
+
|
|
312
|
+
Stores all drift components for audit trail and analysis.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(
|
|
316
|
+
self,
|
|
317
|
+
execution_id: str,
|
|
318
|
+
seed_id: str,
|
|
319
|
+
iteration: int,
|
|
320
|
+
metrics: DriftMetrics,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Create DriftMeasuredEvent.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
execution_id: Unique execution identifier
|
|
326
|
+
seed_id: Seed identifier being measured against
|
|
327
|
+
iteration: Current iteration number
|
|
328
|
+
metrics: Drift measurement results
|
|
329
|
+
"""
|
|
330
|
+
super().__init__(
|
|
331
|
+
type="observability.drift.measured",
|
|
332
|
+
aggregate_type="execution",
|
|
333
|
+
aggregate_id=execution_id,
|
|
334
|
+
data={
|
|
335
|
+
"seed_id": seed_id,
|
|
336
|
+
"iteration": iteration,
|
|
337
|
+
"goal_drift": metrics.goal_drift,
|
|
338
|
+
"constraint_drift": metrics.constraint_drift,
|
|
339
|
+
"ontology_drift": metrics.ontology_drift,
|
|
340
|
+
"combined_drift": metrics.combined_drift,
|
|
341
|
+
"is_acceptable": metrics.is_acceptable,
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class DriftThresholdExceededEvent(BaseEvent):
|
|
347
|
+
"""Event emitted when drift exceeds acceptable threshold.
|
|
348
|
+
|
|
349
|
+
Indicates combined drift > 0.3 (NFR5), may trigger consensus.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
execution_id: str,
|
|
355
|
+
seed_id: str,
|
|
356
|
+
iteration: int,
|
|
357
|
+
metrics: DriftMetrics,
|
|
358
|
+
threshold: float = DRIFT_THRESHOLD,
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Create DriftThresholdExceededEvent.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
execution_id: Unique execution identifier
|
|
364
|
+
seed_id: Seed identifier being measured against
|
|
365
|
+
iteration: Current iteration number
|
|
366
|
+
metrics: Drift measurement results
|
|
367
|
+
threshold: The threshold that was exceeded (default: 0.3)
|
|
368
|
+
"""
|
|
369
|
+
super().__init__(
|
|
370
|
+
type="observability.drift.threshold_exceeded",
|
|
371
|
+
aggregate_type="execution",
|
|
372
|
+
aggregate_id=execution_id,
|
|
373
|
+
data={
|
|
374
|
+
"seed_id": seed_id,
|
|
375
|
+
"iteration": iteration,
|
|
376
|
+
"goal_drift": metrics.goal_drift,
|
|
377
|
+
"constraint_drift": metrics.constraint_drift,
|
|
378
|
+
"ontology_drift": metrics.ontology_drift,
|
|
379
|
+
"combined_drift": metrics.combined_drift,
|
|
380
|
+
"threshold": threshold,
|
|
381
|
+
"exceeded_by": metrics.combined_drift - threshold,
|
|
382
|
+
},
|
|
383
|
+
)
|