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,483 @@
1
+ """TODO Registry for capturing improvements during execution.
2
+
3
+ This module implements Story 7-1: TODO Registry - capturing discovered
4
+ improvements without disrupting the primary execution flow.
5
+
6
+ Design Principles:
7
+ - Non-blocking registration: TODOs are registered asynchronously
8
+ - Event sourcing: All state changes are persisted as events
9
+ - Immutable data: TODO items are frozen dataclasses
10
+ - Result type: Expected failures use Result, not exceptions
11
+
12
+ Usage:
13
+ from ouroboros.secondary import TodoRegistry, Todo, Priority, TodoStatus
14
+ from ouroboros.persistence import EventStore
15
+
16
+ store = EventStore("sqlite+aiosqlite:///events.db")
17
+ await store.initialize()
18
+
19
+ registry = TodoRegistry(store)
20
+
21
+ # Register a TODO (non-blocking)
22
+ await registry.register(
23
+ description="Refactor authentication module",
24
+ context="execution-123",
25
+ priority=Priority.MEDIUM,
26
+ )
27
+
28
+ # Get pending TODOs
29
+ result = await registry.get_pending()
30
+ if result.is_ok:
31
+ for todo in result.value:
32
+ print(f"{todo.priority}: {todo.description}")
33
+ """
34
+
35
+ from dataclasses import dataclass, field
36
+ from datetime import UTC, datetime
37
+ from enum import StrEnum
38
+ from uuid import uuid4
39
+
40
+ from ouroboros.core.errors import PersistenceError
41
+ from ouroboros.core.types import Result
42
+ from ouroboros.events.base import BaseEvent
43
+ from ouroboros.observability.logging import get_logger
44
+ from ouroboros.persistence.event_store import EventStore
45
+
46
+ log = get_logger(__name__)
47
+
48
+
49
+ class Priority(StrEnum):
50
+ """Priority levels for TODO items.
51
+
52
+ Attributes:
53
+ HIGH: Critical improvements that should be addressed first
54
+ MEDIUM: Standard improvements with moderate impact
55
+ LOW: Nice-to-have improvements with minimal urgency
56
+ """
57
+
58
+ HIGH = "high"
59
+ MEDIUM = "medium"
60
+ LOW = "low"
61
+
62
+ @property
63
+ def sort_order(self) -> int:
64
+ """Return numeric sort order (lower = higher priority)."""
65
+ orders = {Priority.HIGH: 0, Priority.MEDIUM: 1, Priority.LOW: 2}
66
+ return orders[self]
67
+
68
+
69
+ class TodoStatus(StrEnum):
70
+ """Lifecycle status of a TODO item.
71
+
72
+ Attributes:
73
+ PENDING: Awaiting processing in secondary loop
74
+ IN_PROGRESS: Currently being processed
75
+ DONE: Successfully completed
76
+ FAILED: Processing attempted but failed
77
+ SKIPPED: Intentionally skipped (user decision or timeout)
78
+ """
79
+
80
+ PENDING = "pending"
81
+ IN_PROGRESS = "in_progress"
82
+ DONE = "done"
83
+ FAILED = "failed"
84
+ SKIPPED = "skipped"
85
+
86
+ @property
87
+ def is_terminal(self) -> bool:
88
+ """Return True if this is a terminal state."""
89
+ return self in (TodoStatus.DONE, TodoStatus.FAILED, TodoStatus.SKIPPED)
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class Todo:
94
+ """Immutable TODO item representing a discovered improvement.
95
+
96
+ Attributes:
97
+ id: Unique identifier (UUID)
98
+ description: Human-readable description of the improvement
99
+ context: Where the TODO was discovered (e.g., execution ID, file path)
100
+ priority: Importance level for processing order
101
+ created_at: When the TODO was registered (UTC)
102
+ status: Current lifecycle status
103
+ error_message: Error details if status is FAILED
104
+ """
105
+
106
+ id: str
107
+ description: str
108
+ context: str
109
+ priority: Priority
110
+ created_at: datetime
111
+ status: TodoStatus = TodoStatus.PENDING
112
+ error_message: str | None = None
113
+
114
+ @classmethod
115
+ def create(
116
+ cls,
117
+ description: str,
118
+ context: str,
119
+ priority: Priority = Priority.MEDIUM,
120
+ ) -> "Todo":
121
+ """Factory method to create a new TODO.
122
+
123
+ Args:
124
+ description: What improvement is needed
125
+ context: Where it was discovered
126
+ priority: How urgently it should be addressed
127
+
128
+ Returns:
129
+ New Todo instance with generated ID and timestamp
130
+ """
131
+ return cls(
132
+ id=str(uuid4()),
133
+ description=description,
134
+ context=context,
135
+ priority=priority,
136
+ created_at=datetime.now(UTC),
137
+ status=TodoStatus.PENDING,
138
+ )
139
+
140
+ def with_status(
141
+ self,
142
+ status: TodoStatus,
143
+ error_message: str | None = None,
144
+ ) -> "Todo":
145
+ """Return a new Todo with updated status.
146
+
147
+ Args:
148
+ status: New status
149
+ error_message: Error details if status is FAILED
150
+
151
+ Returns:
152
+ New Todo instance with updated status
153
+ """
154
+ return Todo(
155
+ id=self.id,
156
+ description=self.description,
157
+ context=self.context,
158
+ priority=self.priority,
159
+ created_at=self.created_at,
160
+ status=status,
161
+ error_message=error_message,
162
+ )
163
+
164
+
165
+ # Event type constants
166
+ EVENT_TODO_CREATED = "todo.created"
167
+ EVENT_TODO_STATUS_CHANGED = "todo.status.changed"
168
+
169
+
170
+ def _create_todo_event(todo: Todo) -> BaseEvent:
171
+ """Create a TODO creation event.
172
+
173
+ Args:
174
+ todo: The TODO that was created
175
+
176
+ Returns:
177
+ BaseEvent for persistence
178
+ """
179
+ return BaseEvent(
180
+ type=EVENT_TODO_CREATED,
181
+ aggregate_type="todo",
182
+ aggregate_id=todo.id,
183
+ data={
184
+ "description": todo.description,
185
+ "context": todo.context,
186
+ "priority": todo.priority.value,
187
+ "created_at": todo.created_at.isoformat(),
188
+ "status": todo.status.value,
189
+ },
190
+ )
191
+
192
+
193
+ def _create_status_change_event(
194
+ todo: Todo,
195
+ old_status: TodoStatus,
196
+ new_status: TodoStatus,
197
+ error_message: str | None = None,
198
+ ) -> BaseEvent:
199
+ """Create a TODO status change event.
200
+
201
+ Args:
202
+ todo: The TODO being updated
203
+ old_status: Previous status
204
+ new_status: New status
205
+ error_message: Error details if transitioning to FAILED
206
+
207
+ Returns:
208
+ BaseEvent for persistence
209
+ """
210
+ data = {
211
+ "old_status": old_status.value,
212
+ "new_status": new_status.value,
213
+ }
214
+ if error_message:
215
+ data["error_message"] = error_message
216
+
217
+ return BaseEvent(
218
+ type=EVENT_TODO_STATUS_CHANGED,
219
+ aggregate_type="todo",
220
+ aggregate_id=todo.id,
221
+ data=data,
222
+ )
223
+
224
+
225
+ def _reconstruct_todo_from_events(events: list[BaseEvent]) -> Todo | None:
226
+ """Reconstruct a Todo from its event history.
227
+
228
+ Args:
229
+ events: List of events for a single TODO aggregate
230
+
231
+ Returns:
232
+ Reconstructed Todo or None if no events
233
+ """
234
+ if not events:
235
+ return None
236
+
237
+ # Find the creation event
238
+ creation_event = None
239
+ for event in events:
240
+ if event.type == EVENT_TODO_CREATED:
241
+ creation_event = event
242
+ break
243
+
244
+ if creation_event is None:
245
+ return None
246
+
247
+ data = creation_event.data
248
+ todo = Todo(
249
+ id=creation_event.aggregate_id,
250
+ description=data["description"],
251
+ context=data["context"],
252
+ priority=Priority(data["priority"]),
253
+ created_at=datetime.fromisoformat(data["created_at"]),
254
+ status=TodoStatus(data["status"]),
255
+ )
256
+
257
+ # Apply status change events
258
+ for event in events:
259
+ if event.type == EVENT_TODO_STATUS_CHANGED:
260
+ todo = todo.with_status(
261
+ TodoStatus(event.data["new_status"]),
262
+ event.data.get("error_message"),
263
+ )
264
+
265
+ return todo
266
+
267
+
268
+ @dataclass
269
+ class TodoRegistry:
270
+ """Registry for TODO items with event-sourced persistence.
271
+
272
+ Provides non-blocking registration of TODOs during execution
273
+ and retrieval for secondary loop processing.
274
+
275
+ Attributes:
276
+ _event_store: EventStore for persistence
277
+ _todo_ids: In-memory index of registered TODO IDs
278
+ """
279
+
280
+ _event_store: EventStore
281
+ _todo_ids: set[str] = field(default_factory=set)
282
+
283
+ async def register(
284
+ self,
285
+ description: str,
286
+ context: str,
287
+ priority: Priority = Priority.MEDIUM,
288
+ ) -> Result[Todo, PersistenceError]:
289
+ """Register a new TODO item.
290
+
291
+ This is a non-blocking operation that persists the TODO
292
+ and returns immediately without waiting for processing.
293
+
294
+ Args:
295
+ description: What improvement is needed
296
+ context: Where it was discovered (e.g., execution ID)
297
+ priority: How urgently it should be addressed
298
+
299
+ Returns:
300
+ Result containing the created Todo or PersistenceError
301
+ """
302
+ todo = Todo.create(description, context, priority)
303
+ event = _create_todo_event(todo)
304
+
305
+ try:
306
+ await self._event_store.append(event)
307
+ self._todo_ids.add(todo.id)
308
+
309
+ log.info(
310
+ "todo.registered",
311
+ todo_id=todo.id,
312
+ priority=priority.value,
313
+ context=context,
314
+ )
315
+
316
+ return Result.ok(todo)
317
+
318
+ except PersistenceError as e:
319
+ log.error(
320
+ "todo.registration.failed",
321
+ todo_id=todo.id,
322
+ error=str(e),
323
+ )
324
+ return Result.err(e)
325
+
326
+ async def update_status(
327
+ self,
328
+ todo_id: str,
329
+ new_status: TodoStatus,
330
+ error_message: str | None = None,
331
+ ) -> Result[Todo, PersistenceError]:
332
+ """Update the status of a TODO item.
333
+
334
+ Args:
335
+ todo_id: ID of the TODO to update
336
+ new_status: New status to set
337
+ error_message: Error details if transitioning to FAILED
338
+
339
+ Returns:
340
+ Result containing the updated Todo or PersistenceError
341
+ """
342
+ # Get current state
343
+ result = await self.get_by_id(todo_id)
344
+ if result.is_err:
345
+ return Result.err(result.error)
346
+
347
+ todo = result.value
348
+ if todo is None:
349
+ return Result.err(
350
+ PersistenceError(
351
+ f"TODO not found: {todo_id}",
352
+ operation="update_status",
353
+ details={"todo_id": todo_id},
354
+ )
355
+ )
356
+
357
+ old_status = todo.status
358
+ event = _create_status_change_event(todo, old_status, new_status, error_message)
359
+
360
+ try:
361
+ await self._event_store.append(event)
362
+ updated_todo = todo.with_status(new_status, error_message)
363
+
364
+ log.info(
365
+ "todo.status.updated",
366
+ todo_id=todo_id,
367
+ old_status=old_status.value,
368
+ new_status=new_status.value,
369
+ )
370
+
371
+ return Result.ok(updated_todo)
372
+
373
+ except PersistenceError as e:
374
+ log.error(
375
+ "todo.status.update.failed",
376
+ todo_id=todo_id,
377
+ error=str(e),
378
+ )
379
+ return Result.err(e)
380
+
381
+ async def get_by_id(self, todo_id: str) -> Result[Todo | None, PersistenceError]:
382
+ """Retrieve a TODO by its ID.
383
+
384
+ Args:
385
+ todo_id: The TODO's unique identifier
386
+
387
+ Returns:
388
+ Result containing the Todo (or None if not found) or PersistenceError
389
+ """
390
+ try:
391
+ events = await self._event_store.replay("todo", todo_id)
392
+ todo = _reconstruct_todo_from_events(events)
393
+ return Result.ok(todo)
394
+
395
+ except PersistenceError as e:
396
+ log.error(
397
+ "todo.retrieval.failed",
398
+ todo_id=todo_id,
399
+ error=str(e),
400
+ )
401
+ return Result.err(e)
402
+
403
+ async def get_pending(
404
+ self,
405
+ limit: int | None = None,
406
+ ) -> Result[list[Todo], PersistenceError]:
407
+ """Retrieve all pending TODOs sorted by priority.
408
+
409
+ Args:
410
+ limit: Maximum number of TODOs to return (None = all)
411
+
412
+ Returns:
413
+ Result containing list of pending Todos sorted by priority
414
+ """
415
+ todos: list[Todo] = []
416
+
417
+ for todo_id in self._todo_ids:
418
+ result = await self.get_by_id(todo_id)
419
+ if result.is_err:
420
+ return Result.err(result.error)
421
+
422
+ todo = result.value
423
+ if todo is not None and todo.status == TodoStatus.PENDING:
424
+ todos.append(todo)
425
+
426
+ # Sort by priority (HIGH first) then by creation time (oldest first)
427
+ todos.sort(key=lambda t: (t.priority.sort_order, t.created_at))
428
+
429
+ if limit is not None:
430
+ todos = todos[:limit]
431
+
432
+ return Result.ok(todos)
433
+
434
+ async def get_all(self) -> Result[list[Todo], PersistenceError]:
435
+ """Retrieve all TODOs regardless of status.
436
+
437
+ Returns:
438
+ Result containing list of all Todos
439
+ """
440
+ todos: list[Todo] = []
441
+
442
+ for todo_id in self._todo_ids:
443
+ result = await self.get_by_id(todo_id)
444
+ if result.is_err:
445
+ return Result.err(result.error)
446
+
447
+ todo = result.value
448
+ if todo is not None:
449
+ todos.append(todo)
450
+
451
+ # Sort by creation time (newest first)
452
+ todos.sort(key=lambda t: t.created_at, reverse=True)
453
+ return Result.ok(todos)
454
+
455
+ def count_pending(self) -> int:
456
+ """Return count of tracked TODO IDs.
457
+
458
+ Note: This is the count of IDs in memory, which may include
459
+ non-pending items. For accurate pending count, use get_pending().
460
+
461
+ Returns:
462
+ Number of TODO IDs tracked in memory
463
+ """
464
+ return len(self._todo_ids)
465
+
466
+ async def get_stats(self) -> Result[dict[str, int], PersistenceError]:
467
+ """Get statistics about TODOs by status.
468
+
469
+ Returns:
470
+ Result containing dict mapping status to count
471
+ """
472
+ stats: dict[str, int] = {status.value: 0 for status in TodoStatus}
473
+
474
+ for todo_id in self._todo_ids:
475
+ result = await self.get_by_id(todo_id)
476
+ if result.is_err:
477
+ return Result.err(result.error)
478
+
479
+ todo = result.value
480
+ if todo is not None:
481
+ stats[todo.status.value] += 1
482
+
483
+ return Result.ok(stats)