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,340 @@
1
+ """Escalation on Failure for Ouroboros.
2
+
3
+ This module implements automatic tier escalation when tasks fail consecutively.
4
+ The escalation policy follows: Frugal -> Standard -> Frontier.
5
+
6
+ After reaching Frontier, if failures continue, a STAGNATION_DETECTED event
7
+ is emitted for the resilience system to handle (lateral thinking path).
8
+
9
+ Escalation Rules:
10
+ - 2 consecutive failures trigger escalation to next tier
11
+ - Success resets the failure counter
12
+ - Never infinite retry (Frontier failure emits event for resilience)
13
+
14
+ Usage:
15
+ from ouroboros.routing.escalation import EscalationManager, FailureTracker
16
+ from ouroboros.routing.tiers import Tier
17
+
18
+ # Create manager
19
+ manager = EscalationManager()
20
+
21
+ # Record failures and check for escalation
22
+ result = manager.record_failure("pattern_123", Tier.FRUGAL)
23
+ if result.is_ok:
24
+ action = result.value
25
+ if action.should_escalate:
26
+ new_tier = action.target_tier
27
+ elif action.is_stagnation:
28
+ # Handle stagnation - emit event
29
+
30
+ # Reset on success
31
+ manager.record_success("pattern_123")
32
+ """
33
+
34
+ from dataclasses import dataclass, field
35
+ from datetime import UTC, datetime
36
+
37
+ from ouroboros.core.types import Result
38
+ from ouroboros.events.base import BaseEvent
39
+ from ouroboros.observability.logging import get_logger
40
+ from ouroboros.routing.tiers import Tier
41
+
42
+ log = get_logger(__name__)
43
+
44
+ # Number of consecutive failures before escalation
45
+ FAILURE_THRESHOLD = 2
46
+
47
+
48
+ @dataclass
49
+ class FailureTracker:
50
+ """Tracks consecutive failures per task pattern.
51
+
52
+ Attributes:
53
+ consecutive_failures: Count of consecutive failures for the pattern.
54
+ current_tier: The current tier being used for the pattern.
55
+ last_failure_time: Timestamp of the most recent failure.
56
+ """
57
+
58
+ consecutive_failures: int = 0
59
+ current_tier: Tier = Tier.FRUGAL
60
+ last_failure_time: datetime | None = None
61
+
62
+ def reset_on_success(self) -> None:
63
+ """Reset failure tracking on success.
64
+
65
+ Clears the consecutive failure counter while preserving the current tier.
66
+ """
67
+ self.consecutive_failures = 0
68
+ self.last_failure_time = None
69
+
70
+ def record_failure(self) -> None:
71
+ """Record a failure occurrence.
72
+
73
+ Increments the consecutive failure counter and updates the timestamp.
74
+ """
75
+ self.consecutive_failures += 1
76
+ self.last_failure_time = datetime.now(UTC)
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class EscalationAction:
81
+ """Result of an escalation decision.
82
+
83
+ Attributes:
84
+ should_escalate: Whether escalation to a higher tier is needed.
85
+ is_stagnation: Whether we've reached Frontier and still failing.
86
+ target_tier: The tier to escalate to (if should_escalate is True).
87
+ previous_tier: The tier before escalation decision.
88
+ failure_count: Number of consecutive failures.
89
+ """
90
+
91
+ should_escalate: bool
92
+ is_stagnation: bool
93
+ target_tier: Tier | None
94
+ previous_tier: Tier
95
+ failure_count: int
96
+
97
+
98
+ class StagnationEvent(BaseEvent):
99
+ """Event emitted when Frontier tier still fails.
100
+
101
+ This event signals the resilience system to engage lateral thinking
102
+ paths instead of continuing vertical escalation.
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ pattern_id: str,
108
+ failure_count: int,
109
+ **kwargs,
110
+ ) -> None:
111
+ """Create a stagnation event.
112
+
113
+ Args:
114
+ pattern_id: The task pattern identifier.
115
+ failure_count: Number of consecutive failures at Frontier.
116
+ **kwargs: Additional event data.
117
+ """
118
+ super().__init__(
119
+ type="escalation.stagnation.detected",
120
+ aggregate_type="routing",
121
+ aggregate_id=pattern_id,
122
+ data={
123
+ "pattern_id": pattern_id,
124
+ "failure_count": failure_count,
125
+ "tier": Tier.FRONTIER.value,
126
+ **kwargs,
127
+ },
128
+ )
129
+
130
+
131
+ @dataclass
132
+ class EscalationManager:
133
+ """Manages tier escalation based on consecutive failures.
134
+
135
+ The manager tracks failures per task pattern and determines when
136
+ escalation is needed. It follows the escalation path:
137
+ Frugal -> Standard -> Frontier -> Stagnation Event
138
+
139
+ Design:
140
+ - Stateful: Maintains failure tracking state per pattern
141
+ - Thread-safe operations should use external synchronization
142
+ - Uses Result type for consistent error handling
143
+
144
+ Usage:
145
+ manager = EscalationManager()
146
+
147
+ # Record failures
148
+ action = manager.record_failure("pattern_123", Tier.FRUGAL)
149
+ if action.is_ok:
150
+ if action.value.should_escalate:
151
+ # Use action.value.target_tier
152
+ elif action.value.is_stagnation:
153
+ # Emit event for resilience system
154
+
155
+ # Record success to reset counter
156
+ manager.record_success("pattern_123")
157
+ """
158
+
159
+ # Internal state: pattern_id -> FailureTracker
160
+ _trackers: dict[str, FailureTracker] = field(default_factory=dict)
161
+
162
+ def _get_next_tier(self, current: Tier) -> Tier | None:
163
+ """Get the next tier in escalation path.
164
+
165
+ Args:
166
+ current: The current tier.
167
+
168
+ Returns:
169
+ The next tier, or None if at Frontier (highest tier).
170
+ """
171
+ escalation_path = {
172
+ Tier.FRUGAL: Tier.STANDARD,
173
+ Tier.STANDARD: Tier.FRONTIER,
174
+ Tier.FRONTIER: None,
175
+ }
176
+ return escalation_path.get(current)
177
+
178
+ def _get_or_create_tracker(self, pattern_id: str, current_tier: Tier) -> FailureTracker:
179
+ """Get or create a failure tracker for a pattern.
180
+
181
+ Args:
182
+ pattern_id: The task pattern identifier.
183
+ current_tier: The current tier being used.
184
+
185
+ Returns:
186
+ The FailureTracker for the pattern.
187
+ """
188
+ if pattern_id not in self._trackers:
189
+ self._trackers[pattern_id] = FailureTracker(current_tier=current_tier)
190
+ return self._trackers[pattern_id]
191
+
192
+ def record_failure(self, pattern_id: str, current_tier: Tier) -> Result[EscalationAction, None]:
193
+ """Record a failure and determine if escalation is needed.
194
+
195
+ Args:
196
+ pattern_id: Unique identifier for the task pattern.
197
+ current_tier: The tier that just failed.
198
+
199
+ Returns:
200
+ Result containing EscalationAction with escalation decision.
201
+ """
202
+ tracker = self._get_or_create_tracker(pattern_id, current_tier)
203
+ tracker.current_tier = current_tier
204
+ tracker.record_failure()
205
+
206
+ failure_count = tracker.consecutive_failures
207
+
208
+ log.debug(
209
+ "escalation.failure.recorded",
210
+ pattern_id=pattern_id,
211
+ tier=current_tier.value,
212
+ consecutive_failures=failure_count,
213
+ )
214
+
215
+ # Check if we've hit the threshold
216
+ if failure_count >= FAILURE_THRESHOLD:
217
+ next_tier = self._get_next_tier(current_tier)
218
+
219
+ if next_tier is not None:
220
+ # Escalate to next tier
221
+ cost_impact = f"{current_tier.cost_multiplier}x -> {next_tier.cost_multiplier}x"
222
+ log.info(
223
+ "escalation.tier.upgraded",
224
+ pattern_id=pattern_id,
225
+ from_tier=current_tier.value,
226
+ to_tier=next_tier.value,
227
+ failure_count=failure_count,
228
+ cost_impact=cost_impact,
229
+ )
230
+
231
+ # Reset failure counter after escalation
232
+ tracker.consecutive_failures = 0
233
+
234
+ return Result.ok(
235
+ EscalationAction(
236
+ should_escalate=True,
237
+ is_stagnation=False,
238
+ target_tier=next_tier,
239
+ previous_tier=current_tier,
240
+ failure_count=failure_count,
241
+ )
242
+ )
243
+ else:
244
+ # Already at Frontier - stagnation detected
245
+ log.warning(
246
+ "escalation.stagnation.detected",
247
+ pattern_id=pattern_id,
248
+ tier=current_tier.value,
249
+ failure_count=failure_count,
250
+ message="Frontier tier failing, lateral thinking needed",
251
+ )
252
+
253
+ return Result.ok(
254
+ EscalationAction(
255
+ should_escalate=False,
256
+ is_stagnation=True,
257
+ target_tier=None,
258
+ previous_tier=current_tier,
259
+ failure_count=failure_count,
260
+ )
261
+ )
262
+
263
+ # Not enough failures yet
264
+ return Result.ok(
265
+ EscalationAction(
266
+ should_escalate=False,
267
+ is_stagnation=False,
268
+ target_tier=None,
269
+ previous_tier=current_tier,
270
+ failure_count=failure_count,
271
+ )
272
+ )
273
+
274
+ def record_success(self, pattern_id: str) -> None:
275
+ """Record a success, resetting the failure counter.
276
+
277
+ Args:
278
+ pattern_id: Unique identifier for the task pattern.
279
+ """
280
+ if pattern_id in self._trackers:
281
+ tracker = self._trackers[pattern_id]
282
+ previous_failures = tracker.consecutive_failures
283
+ tracker.reset_on_success()
284
+
285
+ log.debug(
286
+ "escalation.success.recorded",
287
+ pattern_id=pattern_id,
288
+ previous_failures=previous_failures,
289
+ )
290
+
291
+ def get_tracker(self, pattern_id: str) -> FailureTracker | None:
292
+ """Get the failure tracker for a pattern.
293
+
294
+ Args:
295
+ pattern_id: The task pattern identifier.
296
+
297
+ Returns:
298
+ The FailureTracker if exists, None otherwise.
299
+ """
300
+ return self._trackers.get(pattern_id)
301
+
302
+ def clear_tracker(self, pattern_id: str) -> None:
303
+ """Remove the failure tracker for a pattern.
304
+
305
+ Args:
306
+ pattern_id: The task pattern identifier.
307
+ """
308
+ if pattern_id in self._trackers:
309
+ del self._trackers[pattern_id]
310
+
311
+ def create_stagnation_event(
312
+ self, pattern_id: str, failure_count: int, **additional_data
313
+ ) -> StagnationEvent:
314
+ """Create a stagnation event for the resilience system.
315
+
316
+ This method creates the event; the caller is responsible for
317
+ persisting it to the EventStore.
318
+
319
+ Args:
320
+ pattern_id: The task pattern identifier.
321
+ failure_count: Number of consecutive failures.
322
+ **additional_data: Additional context to include in the event.
323
+
324
+ Returns:
325
+ StagnationEvent ready for persistence.
326
+ """
327
+ event = StagnationEvent(
328
+ pattern_id=pattern_id,
329
+ failure_count=failure_count,
330
+ **additional_data,
331
+ )
332
+
333
+ log.info(
334
+ "escalation.stagnation.event_created",
335
+ event_id=event.id,
336
+ pattern_id=pattern_id,
337
+ failure_count=failure_count,
338
+ )
339
+
340
+ return event
@@ -0,0 +1,204 @@
1
+ """PAL Router for tier selection based on task complexity.
2
+
3
+ This module implements the PAL (Progressive Adaptive LLM) router that determines
4
+ which model tier should handle a task based on its estimated complexity.
5
+
6
+ Routing Thresholds:
7
+ - Complexity < 0.4: Frugal tier (simple tasks)
8
+ - Complexity 0.4-0.7: Standard tier (moderate tasks)
9
+ - Complexity > 0.7: Frontier tier (complex tasks)
10
+
11
+ Design Principles:
12
+ - Stateless: The router holds no internal state. All decisions are based purely
13
+ on the input TaskContext passed to the route() method.
14
+ - Pure Function: Given the same input, the router will always produce the same
15
+ output. This enables easy testing and predictable behavior.
16
+ - Result Type: Returns Result type for consistent error handling.
17
+
18
+ Usage:
19
+ from ouroboros.routing.router import PALRouter
20
+ from ouroboros.routing.complexity import TaskContext
21
+
22
+ router = PALRouter()
23
+
24
+ # Route a simple task
25
+ context = TaskContext(token_count=100, tool_dependencies=[], ac_depth=1)
26
+ result = router.route(context)
27
+ if result.is_ok:
28
+ tier = result.value
29
+ print(f"Route to: {tier.value}") # "frugal"
30
+
31
+ # Alternatively, use the convenience function
32
+ from ouroboros.routing.router import route_task
33
+ result = route_task(context)
34
+ """
35
+
36
+ from dataclasses import dataclass
37
+
38
+ from ouroboros.core.errors import ValidationError
39
+ from ouroboros.core.types import Result
40
+ from ouroboros.observability.logging import get_logger
41
+ from ouroboros.routing.complexity import (
42
+ ComplexityScore,
43
+ TaskContext,
44
+ estimate_complexity,
45
+ )
46
+ from ouroboros.routing.tiers import Tier
47
+
48
+ log = get_logger(__name__)
49
+
50
+
51
+ # Routing thresholds
52
+ THRESHOLD_FRUGAL = 0.4 # Below this -> Frugal
53
+ THRESHOLD_STANDARD = 0.7 # Below this (but >= FRUGAL) -> Standard, above -> Frontier
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class RoutingDecision:
58
+ """Result of a routing decision.
59
+
60
+ Contains the selected tier and the complexity analysis that led to the decision.
61
+
62
+ Attributes:
63
+ tier: The selected model tier (Frugal, Standard, or Frontier).
64
+ complexity: The complexity score that determined the routing.
65
+
66
+ Example:
67
+ decision = RoutingDecision(
68
+ tier=Tier.STANDARD,
69
+ complexity=ComplexityScore(score=0.55, breakdown={...}),
70
+ )
71
+ print(f"Route to {decision.tier.value} (score: {decision.complexity.score})")
72
+ """
73
+
74
+ tier: Tier
75
+ complexity: ComplexityScore
76
+
77
+
78
+ def _select_tier_from_score(score: float) -> Tier:
79
+ """Select the appropriate tier based on complexity score.
80
+
81
+ Args:
82
+ score: Complexity score between 0.0 and 1.0.
83
+
84
+ Returns:
85
+ The appropriate Tier based on threshold comparison.
86
+ """
87
+ if score < THRESHOLD_FRUGAL:
88
+ return Tier.FRUGAL
89
+ if score < THRESHOLD_STANDARD:
90
+ return Tier.STANDARD
91
+ return Tier.FRONTIER
92
+
93
+
94
+ class PALRouter:
95
+ """Stateless router for tier selection based on task complexity.
96
+
97
+ The PAL Router determines which model tier should handle a task by:
98
+ 1. Estimating task complexity from the provided context
99
+ 2. Comparing complexity to routing thresholds
100
+ 3. Returning the appropriate tier
101
+
102
+ This router is completely stateless - it holds no internal state and makes
103
+ all decisions based purely on the input provided to each method call.
104
+ This design enables:
105
+ - Easy unit testing with predictable outputs
106
+ - Thread-safe operation without synchronization
107
+ - Simple reasoning about behavior
108
+
109
+ Routing Thresholds:
110
+ - Complexity < 0.4: Frugal tier
111
+ - Complexity 0.4-0.7: Standard tier
112
+ - Complexity > 0.7: Frontier tier
113
+
114
+ Example:
115
+ router = PALRouter()
116
+
117
+ # Route a simple task
118
+ simple_context = TaskContext(token_count=200, tool_dependencies=[], ac_depth=1)
119
+ result = router.route(simple_context)
120
+ assert result.value.tier == Tier.FRUGAL
121
+
122
+ # Route a complex task
123
+ complex_context = TaskContext(
124
+ token_count=5000,
125
+ tool_dependencies=["git", "docker", "npm", "aws"],
126
+ ac_depth=5,
127
+ )
128
+ result = router.route(complex_context)
129
+ assert result.value.tier == Tier.FRONTIER
130
+ """
131
+
132
+ def route(
133
+ self,
134
+ context: TaskContext,
135
+ ) -> Result[RoutingDecision, ValidationError]:
136
+ """Route a task to the appropriate tier based on complexity.
137
+
138
+ This is a pure function - given the same context, it will always
139
+ return the same routing decision.
140
+
141
+ Args:
142
+ context: Task context containing complexity factors.
143
+ All routing decisions are based solely on this input.
144
+
145
+ Returns:
146
+ Result containing RoutingDecision on success or ValidationError on failure.
147
+
148
+ Example:
149
+ router = PALRouter()
150
+ context = TaskContext(
151
+ token_count=1000,
152
+ tool_dependencies=["git"],
153
+ ac_depth=2,
154
+ )
155
+ result = router.route(context)
156
+ if result.is_ok:
157
+ decision = result.value
158
+ print(f"Tier: {decision.tier.value}")
159
+ print(f"Score: {decision.complexity.score:.2f}")
160
+ """
161
+ # Estimate complexity
162
+ complexity_result = estimate_complexity(context)
163
+ if complexity_result.is_err:
164
+ return Result.err(complexity_result.error)
165
+
166
+ complexity = complexity_result.value
167
+
168
+ # Select tier based on score
169
+ tier = _select_tier_from_score(complexity.score)
170
+
171
+ log.info(
172
+ "routing.decision.made",
173
+ tier=tier.value,
174
+ complexity_score=complexity.score,
175
+ token_count=context.token_count,
176
+ tool_count=len(context.tool_dependencies),
177
+ ac_depth=context.ac_depth,
178
+ )
179
+
180
+ return Result.ok(RoutingDecision(tier=tier, complexity=complexity))
181
+
182
+
183
+ def route_task(context: TaskContext) -> Result[RoutingDecision, ValidationError]:
184
+ """Convenience function to route a task without instantiating PALRouter.
185
+
186
+ This is a pure function wrapper around PALRouter.route() for simple use cases.
187
+
188
+ Args:
189
+ context: Task context containing complexity factors.
190
+
191
+ Returns:
192
+ Result containing RoutingDecision on success or ValidationError on failure.
193
+
194
+ Example:
195
+ from ouroboros.routing.router import route_task
196
+ from ouroboros.routing.complexity import TaskContext
197
+
198
+ context = TaskContext(token_count=500, tool_dependencies=["git"], ac_depth=2)
199
+ result = route_task(context)
200
+ if result.is_ok:
201
+ print(f"Route to: {result.value.tier.value}")
202
+ """
203
+ router = PALRouter()
204
+ return router.route(context)