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,401 @@
|
|
|
1
|
+
"""AC (Acceptance Criterion) tree structure for hierarchical decomposition.
|
|
2
|
+
|
|
3
|
+
This module provides data structures for managing AC hierarchy during
|
|
4
|
+
recursive decomposition. The tree is reconstructed from events using
|
|
5
|
+
event sourcing pattern.
|
|
6
|
+
|
|
7
|
+
Key concepts:
|
|
8
|
+
- ACNode: Individual AC in the tree
|
|
9
|
+
- ACTree: Complete tree structure with traversal methods
|
|
10
|
+
- Max depth: 5 levels (NFR10)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ACStatus(str, Enum):
|
|
22
|
+
"""Lifecycle status of an Acceptance Criterion."""
|
|
23
|
+
|
|
24
|
+
PENDING = "pending" # Not yet analyzed
|
|
25
|
+
ATOMIC = "atomic" # Confirmed atomic, ready for execution
|
|
26
|
+
DECOMPOSED = "decomposed" # Split into children
|
|
27
|
+
EXECUTING = "executing" # Currently in Double Diamond cycle
|
|
28
|
+
COMPLETED = "completed" # Execution finished successfully
|
|
29
|
+
FAILED = "failed" # Execution failed
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ACNode:
|
|
34
|
+
"""Immutable node in the AC decomposition tree.
|
|
35
|
+
|
|
36
|
+
Represents a single acceptance criterion in the hierarchy.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
id: Unique identifier for this AC.
|
|
40
|
+
content: The acceptance criterion text.
|
|
41
|
+
depth: Depth in tree (0 = root, max 5).
|
|
42
|
+
parent_id: ID of parent AC (None for root).
|
|
43
|
+
status: Current lifecycle status.
|
|
44
|
+
is_atomic: Whether this AC is atomic (no further decomposition).
|
|
45
|
+
children_ids: Tuple of child AC IDs (immutable).
|
|
46
|
+
execution_id: Associated execution ID if executing/completed.
|
|
47
|
+
metadata: Additional context (e.g., complexity_score, reasoning).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
content: str
|
|
52
|
+
depth: int
|
|
53
|
+
parent_id: str | None = None
|
|
54
|
+
status: ACStatus = ACStatus.PENDING
|
|
55
|
+
is_atomic: bool = False
|
|
56
|
+
children_ids: tuple[str, ...] = field(default_factory=tuple)
|
|
57
|
+
execution_id: str | None = None
|
|
58
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def create(
|
|
62
|
+
content: str,
|
|
63
|
+
depth: int = 0,
|
|
64
|
+
parent_id: str | None = None,
|
|
65
|
+
) -> ACNode:
|
|
66
|
+
"""Create a new AC node with generated ID.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
content: The acceptance criterion text.
|
|
70
|
+
depth: Depth in tree (default: 0 for root).
|
|
71
|
+
parent_id: Parent AC ID (None for root).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
New ACNode instance.
|
|
75
|
+
"""
|
|
76
|
+
return ACNode(
|
|
77
|
+
id=f"ac_{uuid4().hex[:12]}",
|
|
78
|
+
content=content,
|
|
79
|
+
depth=depth,
|
|
80
|
+
parent_id=parent_id,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def with_status(self, status: ACStatus) -> ACNode:
|
|
84
|
+
"""Return a new ACNode with updated status.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
status: New status to set.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
New ACNode with updated status.
|
|
91
|
+
"""
|
|
92
|
+
return ACNode(
|
|
93
|
+
id=self.id,
|
|
94
|
+
content=self.content,
|
|
95
|
+
depth=self.depth,
|
|
96
|
+
parent_id=self.parent_id,
|
|
97
|
+
status=status,
|
|
98
|
+
is_atomic=self.is_atomic,
|
|
99
|
+
children_ids=self.children_ids,
|
|
100
|
+
execution_id=self.execution_id,
|
|
101
|
+
metadata=self.metadata,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def with_atomic(self, is_atomic: bool) -> ACNode:
|
|
105
|
+
"""Return a new ACNode with atomic flag set.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
is_atomic: Whether this AC is atomic.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
New ACNode with updated is_atomic flag.
|
|
112
|
+
"""
|
|
113
|
+
new_status = ACStatus.ATOMIC if is_atomic else self.status
|
|
114
|
+
return ACNode(
|
|
115
|
+
id=self.id,
|
|
116
|
+
content=self.content,
|
|
117
|
+
depth=self.depth,
|
|
118
|
+
parent_id=self.parent_id,
|
|
119
|
+
status=new_status,
|
|
120
|
+
is_atomic=is_atomic,
|
|
121
|
+
children_ids=self.children_ids,
|
|
122
|
+
execution_id=self.execution_id,
|
|
123
|
+
metadata=self.metadata,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def with_children(self, children_ids: tuple[str, ...]) -> ACNode:
|
|
127
|
+
"""Return a new ACNode with children set.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
children_ids: Tuple of child AC IDs.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
New ACNode with children and DECOMPOSED status.
|
|
134
|
+
"""
|
|
135
|
+
return ACNode(
|
|
136
|
+
id=self.id,
|
|
137
|
+
content=self.content,
|
|
138
|
+
depth=self.depth,
|
|
139
|
+
parent_id=self.parent_id,
|
|
140
|
+
status=ACStatus.DECOMPOSED,
|
|
141
|
+
is_atomic=False,
|
|
142
|
+
children_ids=children_ids,
|
|
143
|
+
execution_id=self.execution_id,
|
|
144
|
+
metadata=self.metadata,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def with_execution_id(self, execution_id: str) -> ACNode:
|
|
148
|
+
"""Return a new ACNode with execution ID set.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
execution_id: The execution ID to associate.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
New ACNode with execution ID set.
|
|
155
|
+
"""
|
|
156
|
+
return ACNode(
|
|
157
|
+
id=self.id,
|
|
158
|
+
content=self.content,
|
|
159
|
+
depth=self.depth,
|
|
160
|
+
parent_id=self.parent_id,
|
|
161
|
+
status=ACStatus.EXECUTING,
|
|
162
|
+
is_atomic=self.is_atomic,
|
|
163
|
+
children_ids=self.children_ids,
|
|
164
|
+
execution_id=execution_id,
|
|
165
|
+
metadata=self.metadata,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(slots=True)
|
|
170
|
+
class ACTree:
|
|
171
|
+
"""AC decomposition tree structure.
|
|
172
|
+
|
|
173
|
+
Mutable container for managing AC hierarchy.
|
|
174
|
+
Can be reconstructed from events via event replay.
|
|
175
|
+
|
|
176
|
+
Attributes:
|
|
177
|
+
root_id: ID of the root AC.
|
|
178
|
+
nodes: Mapping from AC ID to ACNode.
|
|
179
|
+
max_depth: Maximum allowed depth (default: 5).
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
root_id: str | None = None
|
|
183
|
+
nodes: dict[str, ACNode] = field(default_factory=dict)
|
|
184
|
+
max_depth: int = 5
|
|
185
|
+
|
|
186
|
+
def add_node(self, node: ACNode) -> None:
|
|
187
|
+
"""Add a node to the tree.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
node: The ACNode to add.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If depth exceeds max_depth.
|
|
194
|
+
"""
|
|
195
|
+
if node.depth > self.max_depth:
|
|
196
|
+
msg = f"Node depth {node.depth} exceeds max depth {self.max_depth}"
|
|
197
|
+
raise ValueError(msg)
|
|
198
|
+
|
|
199
|
+
self.nodes[node.id] = node
|
|
200
|
+
|
|
201
|
+
# Set root if this is the first node or depth 0
|
|
202
|
+
if self.root_id is None or node.depth == 0:
|
|
203
|
+
self.root_id = node.id
|
|
204
|
+
|
|
205
|
+
def get_node(self, ac_id: str) -> ACNode | None:
|
|
206
|
+
"""Get a node by ID.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
ac_id: The AC ID to look up.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
The ACNode if found, None otherwise.
|
|
213
|
+
"""
|
|
214
|
+
return self.nodes.get(ac_id)
|
|
215
|
+
|
|
216
|
+
def update_node(self, node: ACNode) -> None:
|
|
217
|
+
"""Update an existing node.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
node: The updated ACNode (must have same ID).
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
KeyError: If node ID doesn't exist in tree.
|
|
224
|
+
"""
|
|
225
|
+
if node.id not in self.nodes:
|
|
226
|
+
msg = f"Node {node.id} not found in tree"
|
|
227
|
+
raise KeyError(msg)
|
|
228
|
+
self.nodes[node.id] = node
|
|
229
|
+
|
|
230
|
+
def get_children(self, ac_id: str) -> list[ACNode]:
|
|
231
|
+
"""Get all child nodes of an AC.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
ac_id: Parent AC ID.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of child ACNodes.
|
|
238
|
+
"""
|
|
239
|
+
node = self.nodes.get(ac_id)
|
|
240
|
+
if node is None:
|
|
241
|
+
return []
|
|
242
|
+
return [self.nodes[cid] for cid in node.children_ids if cid in self.nodes]
|
|
243
|
+
|
|
244
|
+
def get_ancestors(self, ac_id: str) -> list[ACNode]:
|
|
245
|
+
"""Get all ancestor nodes from root to parent.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
ac_id: The AC ID to find ancestors for.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of ancestor ACNodes from root to immediate parent.
|
|
252
|
+
"""
|
|
253
|
+
ancestors: list[ACNode] = []
|
|
254
|
+
node = self.nodes.get(ac_id)
|
|
255
|
+
|
|
256
|
+
while node and node.parent_id:
|
|
257
|
+
parent = self.nodes.get(node.parent_id)
|
|
258
|
+
if parent:
|
|
259
|
+
ancestors.insert(0, parent) # Insert at beginning for root-first order
|
|
260
|
+
node = parent
|
|
261
|
+
|
|
262
|
+
return ancestors
|
|
263
|
+
|
|
264
|
+
def get_path(self, ac_id: str) -> list[ACNode]:
|
|
265
|
+
"""Get the full path from root to the given AC.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
ac_id: The target AC ID.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of ACNodes from root to target (inclusive).
|
|
272
|
+
"""
|
|
273
|
+
node = self.nodes.get(ac_id)
|
|
274
|
+
if node is None:
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
path = self.get_ancestors(ac_id)
|
|
278
|
+
path.append(node)
|
|
279
|
+
return path
|
|
280
|
+
|
|
281
|
+
def get_leaves(self) -> list[ACNode]:
|
|
282
|
+
"""Get all leaf nodes (no children).
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of leaf ACNodes.
|
|
286
|
+
"""
|
|
287
|
+
return [node for node in self.nodes.values() if not node.children_ids]
|
|
288
|
+
|
|
289
|
+
def get_atomic_nodes(self) -> list[ACNode]:
|
|
290
|
+
"""Get all nodes marked as atomic.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of atomic ACNodes.
|
|
294
|
+
"""
|
|
295
|
+
return [node for node in self.nodes.values() if node.is_atomic]
|
|
296
|
+
|
|
297
|
+
def get_pending_nodes(self) -> list[ACNode]:
|
|
298
|
+
"""Get all nodes with PENDING status.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of pending ACNodes.
|
|
302
|
+
"""
|
|
303
|
+
return [node for node in self.nodes.values() if node.status == ACStatus.PENDING]
|
|
304
|
+
|
|
305
|
+
def can_decompose(self, ac_id: str) -> bool:
|
|
306
|
+
"""Check if an AC can be decomposed.
|
|
307
|
+
|
|
308
|
+
An AC can be decomposed if:
|
|
309
|
+
- It exists in the tree
|
|
310
|
+
- Its depth is less than max_depth
|
|
311
|
+
- It's not already decomposed
|
|
312
|
+
- It's not marked as atomic
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
ac_id: The AC ID to check.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
True if decomposition is allowed.
|
|
319
|
+
"""
|
|
320
|
+
node = self.nodes.get(ac_id)
|
|
321
|
+
if node is None:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
node.depth < self.max_depth
|
|
326
|
+
and node.status not in (ACStatus.DECOMPOSED, ACStatus.COMPLETED)
|
|
327
|
+
and not node.is_atomic
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def is_cyclic(self, parent_content: str, child_content: str) -> bool:
|
|
331
|
+
"""Check if decomposition would create a cycle.
|
|
332
|
+
|
|
333
|
+
Simple check: child content should not be identical to parent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
parent_content: Parent AC content.
|
|
337
|
+
child_content: Proposed child AC content.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True if this would create a cycle.
|
|
341
|
+
"""
|
|
342
|
+
# Normalize and compare
|
|
343
|
+
parent_normalized = parent_content.strip().lower()
|
|
344
|
+
child_normalized = child_content.strip().lower()
|
|
345
|
+
return parent_normalized == child_normalized
|
|
346
|
+
|
|
347
|
+
def to_dict(self) -> dict[str, Any]:
|
|
348
|
+
"""Serialize tree to dictionary.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dictionary representation for persistence.
|
|
352
|
+
"""
|
|
353
|
+
return {
|
|
354
|
+
"root_id": self.root_id,
|
|
355
|
+
"max_depth": self.max_depth,
|
|
356
|
+
"nodes": {
|
|
357
|
+
ac_id: {
|
|
358
|
+
"id": node.id,
|
|
359
|
+
"content": node.content,
|
|
360
|
+
"depth": node.depth,
|
|
361
|
+
"parent_id": node.parent_id,
|
|
362
|
+
"status": node.status.value,
|
|
363
|
+
"is_atomic": node.is_atomic,
|
|
364
|
+
"children_ids": list(node.children_ids),
|
|
365
|
+
"execution_id": node.execution_id,
|
|
366
|
+
"metadata": node.metadata,
|
|
367
|
+
}
|
|
368
|
+
for ac_id, node in self.nodes.items()
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def from_dict(cls, data: dict[str, Any]) -> ACTree:
|
|
374
|
+
"""Deserialize tree from dictionary.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
data: Dictionary from to_dict().
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Reconstructed ACTree.
|
|
381
|
+
"""
|
|
382
|
+
tree = cls(
|
|
383
|
+
root_id=data.get("root_id"),
|
|
384
|
+
max_depth=data.get("max_depth", 5),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for node_data in data.get("nodes", {}).values():
|
|
388
|
+
node = ACNode(
|
|
389
|
+
id=node_data["id"],
|
|
390
|
+
content=node_data["content"],
|
|
391
|
+
depth=node_data["depth"],
|
|
392
|
+
parent_id=node_data.get("parent_id"),
|
|
393
|
+
status=ACStatus(node_data.get("status", "pending")),
|
|
394
|
+
is_atomic=node_data.get("is_atomic", False),
|
|
395
|
+
children_ids=tuple(node_data.get("children_ids", [])),
|
|
396
|
+
execution_id=node_data.get("execution_id"),
|
|
397
|
+
metadata=node_data.get("metadata", {}),
|
|
398
|
+
)
|
|
399
|
+
tree.nodes[node.id] = node
|
|
400
|
+
|
|
401
|
+
return tree
|