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.

Files changed (81) hide show
  1. ouroboros/__init__.py +15 -0
  2. ouroboros/__main__.py +9 -0
  3. ouroboros/bigbang/__init__.py +39 -0
  4. ouroboros/bigbang/ambiguity.py +464 -0
  5. ouroboros/bigbang/interview.py +530 -0
  6. ouroboros/bigbang/seed_generator.py +610 -0
  7. ouroboros/cli/__init__.py +9 -0
  8. ouroboros/cli/commands/__init__.py +7 -0
  9. ouroboros/cli/commands/config.py +79 -0
  10. ouroboros/cli/commands/init.py +425 -0
  11. ouroboros/cli/commands/run.py +201 -0
  12. ouroboros/cli/commands/status.py +85 -0
  13. ouroboros/cli/formatters/__init__.py +31 -0
  14. ouroboros/cli/formatters/panels.py +157 -0
  15. ouroboros/cli/formatters/progress.py +112 -0
  16. ouroboros/cli/formatters/tables.py +166 -0
  17. ouroboros/cli/main.py +60 -0
  18. ouroboros/config/__init__.py +81 -0
  19. ouroboros/config/loader.py +292 -0
  20. ouroboros/config/models.py +332 -0
  21. ouroboros/core/__init__.py +62 -0
  22. ouroboros/core/ac_tree.py +401 -0
  23. ouroboros/core/context.py +472 -0
  24. ouroboros/core/errors.py +246 -0
  25. ouroboros/core/seed.py +212 -0
  26. ouroboros/core/types.py +205 -0
  27. ouroboros/evaluation/__init__.py +110 -0
  28. ouroboros/evaluation/consensus.py +350 -0
  29. ouroboros/evaluation/mechanical.py +351 -0
  30. ouroboros/evaluation/models.py +235 -0
  31. ouroboros/evaluation/pipeline.py +286 -0
  32. ouroboros/evaluation/semantic.py +302 -0
  33. ouroboros/evaluation/trigger.py +278 -0
  34. ouroboros/events/__init__.py +5 -0
  35. ouroboros/events/base.py +80 -0
  36. ouroboros/events/decomposition.py +153 -0
  37. ouroboros/events/evaluation.py +248 -0
  38. ouroboros/execution/__init__.py +44 -0
  39. ouroboros/execution/atomicity.py +451 -0
  40. ouroboros/execution/decomposition.py +481 -0
  41. ouroboros/execution/double_diamond.py +1386 -0
  42. ouroboros/execution/subagent.py +275 -0
  43. ouroboros/observability/__init__.py +63 -0
  44. ouroboros/observability/drift.py +383 -0
  45. ouroboros/observability/logging.py +504 -0
  46. ouroboros/observability/retrospective.py +338 -0
  47. ouroboros/orchestrator/__init__.py +78 -0
  48. ouroboros/orchestrator/adapter.py +391 -0
  49. ouroboros/orchestrator/events.py +278 -0
  50. ouroboros/orchestrator/runner.py +597 -0
  51. ouroboros/orchestrator/session.py +486 -0
  52. ouroboros/persistence/__init__.py +23 -0
  53. ouroboros/persistence/checkpoint.py +511 -0
  54. ouroboros/persistence/event_store.py +183 -0
  55. ouroboros/persistence/migrations/__init__.py +1 -0
  56. ouroboros/persistence/migrations/runner.py +100 -0
  57. ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
  58. ouroboros/persistence/schema.py +56 -0
  59. ouroboros/persistence/uow.py +230 -0
  60. ouroboros/providers/__init__.py +28 -0
  61. ouroboros/providers/base.py +133 -0
  62. ouroboros/providers/claude_code_adapter.py +212 -0
  63. ouroboros/providers/litellm_adapter.py +316 -0
  64. ouroboros/py.typed +0 -0
  65. ouroboros/resilience/__init__.py +67 -0
  66. ouroboros/resilience/lateral.py +595 -0
  67. ouroboros/resilience/stagnation.py +727 -0
  68. ouroboros/routing/__init__.py +60 -0
  69. ouroboros/routing/complexity.py +272 -0
  70. ouroboros/routing/downgrade.py +664 -0
  71. ouroboros/routing/escalation.py +340 -0
  72. ouroboros/routing/router.py +204 -0
  73. ouroboros/routing/tiers.py +247 -0
  74. ouroboros/secondary/__init__.py +40 -0
  75. ouroboros/secondary/scheduler.py +467 -0
  76. ouroboros/secondary/todo_registry.py +483 -0
  77. ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
  78. ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
  79. ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
  80. ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
  81. 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
+ )