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,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)
|