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,1386 @@
|
|
|
1
|
+
"""Double Diamond cycle implementation for Phase 2 Execution.
|
|
2
|
+
|
|
3
|
+
The Double Diamond is a design thinking pattern with four phases:
|
|
4
|
+
1. Discover (Diverge): Explore problem space, gather insights
|
|
5
|
+
2. Define (Converge): Converge on approach, filter through ontology
|
|
6
|
+
3. Design (Diverge): Create solution options
|
|
7
|
+
4. Deliver (Converge): Implement and validate, filter through ontology
|
|
8
|
+
|
|
9
|
+
This module implements:
|
|
10
|
+
- Phase enum with phase metadata (divergent/convergent, ordering)
|
|
11
|
+
- PhaseContext for passing state between phases
|
|
12
|
+
- PhaseResult for capturing phase outputs
|
|
13
|
+
- DoubleDiamond class orchestrating the full cycle
|
|
14
|
+
- Retry logic with exponential backoff for phase failures
|
|
15
|
+
- Event emission for phase transitions and cycle lifecycle
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from ouroboros.execution.double_diamond import DoubleDiamond
|
|
19
|
+
|
|
20
|
+
dd = DoubleDiamond(llm_adapter=adapter)
|
|
21
|
+
result = await dd.run_cycle(
|
|
22
|
+
execution_id="exec-123",
|
|
23
|
+
seed_id="seed-456",
|
|
24
|
+
current_ac="Implement user authentication",
|
|
25
|
+
iteration=1,
|
|
26
|
+
)
|
|
27
|
+
if result.is_ok:
|
|
28
|
+
cycle_result = result.value
|
|
29
|
+
print(f"Cycle completed: {cycle_result.success}")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import asyncio
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from typing import TYPE_CHECKING, Any
|
|
38
|
+
|
|
39
|
+
from ouroboros.core.errors import OuroborosError, ProviderError
|
|
40
|
+
from ouroboros.core.types import Result
|
|
41
|
+
from ouroboros.events.base import BaseEvent
|
|
42
|
+
from ouroboros.observability.logging import get_logger
|
|
43
|
+
from ouroboros.resilience.stagnation import (
|
|
44
|
+
ExecutionHistory,
|
|
45
|
+
StagnationDetector,
|
|
46
|
+
create_stagnation_event,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from ouroboros.providers.litellm_adapter import LiteLLMAdapter
|
|
51
|
+
|
|
52
|
+
log = get_logger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Phase-specific prompts (extracted for maintainability)
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
PHASE_PROMPTS: dict[str, dict[str, str]] = {
|
|
60
|
+
"discover": {
|
|
61
|
+
"system": """You are an expert problem analyst in the Discover phase of the Double Diamond process.
|
|
62
|
+
Your role is to DIVERGE - explore the problem space widely and gather insights.
|
|
63
|
+
|
|
64
|
+
Guidelines:
|
|
65
|
+
- Ask clarifying questions about requirements
|
|
66
|
+
- Identify potential challenges and risks
|
|
67
|
+
- Explore different perspectives on the problem
|
|
68
|
+
- List assumptions that need validation
|
|
69
|
+
- Note any ambiguities or unknowns""",
|
|
70
|
+
"user_template": """Acceptance Criterion: {current_ac}
|
|
71
|
+
|
|
72
|
+
Execution ID: {execution_id}
|
|
73
|
+
Iteration: {iteration}
|
|
74
|
+
|
|
75
|
+
Explore this problem space. What insights, questions, challenges, and considerations emerge?""",
|
|
76
|
+
"output_key": "insights",
|
|
77
|
+
"event_data_key": "insights_generated",
|
|
78
|
+
},
|
|
79
|
+
"define": {
|
|
80
|
+
"system": """You are an expert analyst in the Define phase of the Double Diamond process.
|
|
81
|
+
Your role is to CONVERGE - narrow down and define the approach based on insights gathered.
|
|
82
|
+
|
|
83
|
+
Guidelines:
|
|
84
|
+
- Synthesize insights from the Discover phase
|
|
85
|
+
- Define clear requirements and constraints
|
|
86
|
+
- Prioritize what's most important
|
|
87
|
+
- Make decisions on approach
|
|
88
|
+
- Apply ontology filter: ensure alignment with domain concepts""",
|
|
89
|
+
"user_template": """Acceptance Criterion: {current_ac}
|
|
90
|
+
|
|
91
|
+
Discover Phase Output:
|
|
92
|
+
{previous_output}
|
|
93
|
+
|
|
94
|
+
Based on the insights gathered, define the approach. What requirements, constraints, and priorities emerge?""",
|
|
95
|
+
"output_key": "approach",
|
|
96
|
+
"event_data_key": "approach_defined",
|
|
97
|
+
"previous_phase": "discover",
|
|
98
|
+
},
|
|
99
|
+
"design": {
|
|
100
|
+
"system": """You are an expert solution architect in the Design phase of the Double Diamond process.
|
|
101
|
+
Your role is to DIVERGE - create multiple solution options and explore possibilities.
|
|
102
|
+
|
|
103
|
+
Guidelines:
|
|
104
|
+
- Generate multiple solution approaches
|
|
105
|
+
- Consider trade-offs for each approach
|
|
106
|
+
- Be creative and explore alternatives
|
|
107
|
+
- Include both conventional and innovative solutions
|
|
108
|
+
- Document assumptions for each approach""",
|
|
109
|
+
"user_template": """Acceptance Criterion: {current_ac}
|
|
110
|
+
|
|
111
|
+
Define Phase Output:
|
|
112
|
+
{previous_output}
|
|
113
|
+
|
|
114
|
+
Design solution options. What approaches can address the defined requirements?""",
|
|
115
|
+
"output_key": "solution",
|
|
116
|
+
"event_data_key": "solutions_designed",
|
|
117
|
+
"previous_phase": "define",
|
|
118
|
+
},
|
|
119
|
+
"deliver": {
|
|
120
|
+
"system": """You are an expert implementer in the Deliver phase of the Double Diamond process.
|
|
121
|
+
Your role is to CONVERGE - select the best solution and implement it.
|
|
122
|
+
|
|
123
|
+
Guidelines:
|
|
124
|
+
- Select the most appropriate solution from Design phase
|
|
125
|
+
- Provide concrete implementation details
|
|
126
|
+
- Validate the solution meets acceptance criteria
|
|
127
|
+
- Apply ontology filter: ensure alignment with domain concepts
|
|
128
|
+
- Document what was implemented and why""",
|
|
129
|
+
"user_template": """Acceptance Criterion: {current_ac}
|
|
130
|
+
|
|
131
|
+
Design Phase Output:
|
|
132
|
+
{previous_output}
|
|
133
|
+
|
|
134
|
+
Deliver the solution. Select the best approach and provide implementation details.""",
|
|
135
|
+
"output_key": "result",
|
|
136
|
+
"event_data_key": "delivery_completed",
|
|
137
|
+
"previous_phase": "design",
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# Phase Enum
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Phase(str, Enum):
|
|
148
|
+
"""Double Diamond phase enumeration.
|
|
149
|
+
|
|
150
|
+
Four phases with alternating diverge/converge pattern:
|
|
151
|
+
- DISCOVER: Diverge - explore problem space
|
|
152
|
+
- DEFINE: Converge - narrow down approach (ontology filter active)
|
|
153
|
+
- DESIGN: Diverge - create solution options
|
|
154
|
+
- DELIVER: Converge - implement and validate (ontology filter active)
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
is_divergent: True for Discover and Design phases
|
|
158
|
+
is_convergent: True for Define and Deliver phases
|
|
159
|
+
next_phase: The next phase in sequence (None for DELIVER)
|
|
160
|
+
order: Numeric ordering for sorting (0-3)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
DISCOVER = "discover"
|
|
164
|
+
DEFINE = "define"
|
|
165
|
+
DESIGN = "design"
|
|
166
|
+
DELIVER = "deliver"
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def is_divergent(self) -> bool:
|
|
170
|
+
"""Return True if this is a divergent phase (Discover, Design)."""
|
|
171
|
+
return self in (Phase.DISCOVER, Phase.DESIGN)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def is_convergent(self) -> bool:
|
|
175
|
+
"""Return True if this is a convergent phase (Define, Deliver)."""
|
|
176
|
+
return self in (Phase.DEFINE, Phase.DELIVER)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def next_phase(self) -> Phase | None:
|
|
180
|
+
"""Return the next phase in sequence, or None if this is DELIVER."""
|
|
181
|
+
sequence = {
|
|
182
|
+
Phase.DISCOVER: Phase.DEFINE,
|
|
183
|
+
Phase.DEFINE: Phase.DESIGN,
|
|
184
|
+
Phase.DESIGN: Phase.DELIVER,
|
|
185
|
+
Phase.DELIVER: None,
|
|
186
|
+
}
|
|
187
|
+
return sequence[self]
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def order(self) -> int:
|
|
191
|
+
"""Return numeric order for sorting (0-3)."""
|
|
192
|
+
ordering = {
|
|
193
|
+
Phase.DISCOVER: 0,
|
|
194
|
+
Phase.DEFINE: 1,
|
|
195
|
+
Phase.DESIGN: 2,
|
|
196
|
+
Phase.DELIVER: 3,
|
|
197
|
+
}
|
|
198
|
+
return ordering[self]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# =============================================================================
|
|
202
|
+
# Data Models (frozen for immutability)
|
|
203
|
+
# =============================================================================
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class PhaseResult:
|
|
208
|
+
"""Result of executing a single phase.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
phase: The phase that was executed.
|
|
212
|
+
success: Whether the phase completed successfully.
|
|
213
|
+
output: Phase-specific output data.
|
|
214
|
+
events: Events emitted during phase execution.
|
|
215
|
+
error_message: Error message if phase failed.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
phase: Phase
|
|
219
|
+
success: bool
|
|
220
|
+
output: dict[str, Any]
|
|
221
|
+
events: list[BaseEvent]
|
|
222
|
+
error_message: str | None = None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True)
|
|
226
|
+
class PhaseContext:
|
|
227
|
+
"""Context for phase execution.
|
|
228
|
+
|
|
229
|
+
Contains all state needed to execute a phase, including results
|
|
230
|
+
from previous phases in the current cycle.
|
|
231
|
+
|
|
232
|
+
Attributes:
|
|
233
|
+
execution_id: Unique identifier for this execution.
|
|
234
|
+
seed_id: Identifier of the seed being executed.
|
|
235
|
+
current_ac: The acceptance criterion being worked on.
|
|
236
|
+
phase: The current phase being executed.
|
|
237
|
+
iteration: Current iteration number.
|
|
238
|
+
previous_results: Results from phases completed so far in this cycle.
|
|
239
|
+
Note: While the dataclass is frozen, the dict contents are mutable.
|
|
240
|
+
Callers should not modify this dict after construction.
|
|
241
|
+
depth: Current depth in AC decomposition tree (0 = root).
|
|
242
|
+
parent_ac: Parent AC content if this is a child AC.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
execution_id: str
|
|
246
|
+
seed_id: str
|
|
247
|
+
current_ac: str
|
|
248
|
+
phase: Phase
|
|
249
|
+
iteration: int
|
|
250
|
+
previous_results: dict[Phase, PhaseResult] = field(default_factory=dict)
|
|
251
|
+
depth: int = 0
|
|
252
|
+
parent_ac: str | None = None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True, slots=True)
|
|
256
|
+
class CycleResult:
|
|
257
|
+
"""Result of a complete Double Diamond cycle.
|
|
258
|
+
|
|
259
|
+
Aggregates results from all four phases.
|
|
260
|
+
|
|
261
|
+
Attributes:
|
|
262
|
+
execution_id: Unique identifier for this execution.
|
|
263
|
+
seed_id: Identifier of the seed being executed.
|
|
264
|
+
current_ac: The acceptance criterion that was worked on.
|
|
265
|
+
success: Whether the full cycle completed successfully.
|
|
266
|
+
phase_results: Results from each phase.
|
|
267
|
+
events: All events emitted during the cycle (including cycle-level events).
|
|
268
|
+
is_decomposed: Whether this AC was decomposed into children.
|
|
269
|
+
child_results: Results from child AC cycles (if decomposed).
|
|
270
|
+
depth: Depth at which this cycle was executed.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
execution_id: str
|
|
274
|
+
seed_id: str
|
|
275
|
+
current_ac: str
|
|
276
|
+
success: bool
|
|
277
|
+
phase_results: dict[Phase, PhaseResult]
|
|
278
|
+
events: list[BaseEvent]
|
|
279
|
+
is_decomposed: bool = False
|
|
280
|
+
child_results: tuple["CycleResult", ...] = field(default_factory=tuple)
|
|
281
|
+
depth: int = 0
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def final_output(self) -> dict[str, Any]:
|
|
285
|
+
"""Return the output from the DELIVER phase (final result)."""
|
|
286
|
+
if Phase.DELIVER in self.phase_results:
|
|
287
|
+
return self.phase_results[Phase.DELIVER].output
|
|
288
|
+
return {}
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def all_events(self) -> list[BaseEvent]:
|
|
292
|
+
"""Return all events including from child cycles."""
|
|
293
|
+
events = list(self.events)
|
|
294
|
+
for child in self.child_results:
|
|
295
|
+
events.extend(child.all_events)
|
|
296
|
+
return events
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# =============================================================================
|
|
300
|
+
# Errors
|
|
301
|
+
# =============================================================================
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ExecutionError(OuroborosError):
|
|
305
|
+
"""Error during Double Diamond execution.
|
|
306
|
+
|
|
307
|
+
Attributes:
|
|
308
|
+
phase: The phase that failed.
|
|
309
|
+
attempt: The retry attempt number.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def __init__(
|
|
313
|
+
self,
|
|
314
|
+
message: str,
|
|
315
|
+
*,
|
|
316
|
+
phase: Phase | None = None,
|
|
317
|
+
attempt: int = 0,
|
|
318
|
+
details: dict[str, Any] | None = None,
|
|
319
|
+
) -> None:
|
|
320
|
+
super().__init__(message, details)
|
|
321
|
+
self.phase = phase
|
|
322
|
+
self.attempt = attempt
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# =============================================================================
|
|
326
|
+
# DoubleDiamond Orchestrator
|
|
327
|
+
# =============================================================================
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class DoubleDiamond:
|
|
331
|
+
"""Orchestrator for the Double Diamond execution cycle.
|
|
332
|
+
|
|
333
|
+
Manages the four-phase cycle with retry logic and event emission.
|
|
334
|
+
|
|
335
|
+
Attributes:
|
|
336
|
+
llm_adapter: LLM adapter for calling language models.
|
|
337
|
+
default_model: Default model for LLM calls (can be overridden per-phase).
|
|
338
|
+
temperature: Temperature setting for LLM calls.
|
|
339
|
+
max_tokens: Maximum tokens for LLM responses.
|
|
340
|
+
max_retries: Maximum retry attempts per phase (default: 3).
|
|
341
|
+
base_delay: Base delay in seconds for exponential backoff (default: 2.0).
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
# Default model - can be overridden via __init__ for PAL router integration
|
|
345
|
+
DEFAULT_MODEL = "openrouter/google/gemini-2.0-flash-001"
|
|
346
|
+
DEFAULT_TEMPERATURE = 0.7
|
|
347
|
+
DEFAULT_MAX_TOKENS = 4096
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
llm_adapter: LiteLLMAdapter,
|
|
352
|
+
*,
|
|
353
|
+
default_model: str | None = None,
|
|
354
|
+
temperature: float | None = None,
|
|
355
|
+
max_tokens: int | None = None,
|
|
356
|
+
max_retries: int = 3,
|
|
357
|
+
base_delay: float = 2.0,
|
|
358
|
+
enable_stagnation_detection: bool = True,
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Initialize DoubleDiamond.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
llm_adapter: LLM adapter for calling language models.
|
|
364
|
+
default_model: Model identifier for LLM calls. Defaults to Gemini Flash.
|
|
365
|
+
Can be overridden for PAL router tier integration.
|
|
366
|
+
temperature: Temperature for LLM sampling. Defaults to 0.7.
|
|
367
|
+
max_tokens: Maximum tokens for LLM responses. Defaults to 4096.
|
|
368
|
+
max_retries: Maximum retry attempts per phase.
|
|
369
|
+
base_delay: Base delay in seconds for exponential backoff.
|
|
370
|
+
enable_stagnation_detection: Enable stagnation pattern detection.
|
|
371
|
+
"""
|
|
372
|
+
self._llm_adapter = llm_adapter
|
|
373
|
+
self._default_model = default_model or self.DEFAULT_MODEL
|
|
374
|
+
self._temperature = temperature if temperature is not None else self.DEFAULT_TEMPERATURE
|
|
375
|
+
self._max_tokens = max_tokens if max_tokens is not None else self.DEFAULT_MAX_TOKENS
|
|
376
|
+
self._max_retries = max_retries
|
|
377
|
+
self._base_delay = base_delay
|
|
378
|
+
self._enable_stagnation_detection = enable_stagnation_detection
|
|
379
|
+
self._stagnation_detector = StagnationDetector() if enable_stagnation_detection else None
|
|
380
|
+
|
|
381
|
+
# Execution history for stagnation detection (per execution_id)
|
|
382
|
+
# Mutable state: tracks recent outputs/errors for pattern detection
|
|
383
|
+
self._execution_histories: dict[str, dict[str, list]] = {}
|
|
384
|
+
|
|
385
|
+
def _get_execution_history(self, execution_id: str) -> dict[str, list]:
|
|
386
|
+
"""Get or create execution history for stagnation detection.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
execution_id: Execution identifier.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Mutable dict with 'outputs', 'errors', 'drifts' lists.
|
|
393
|
+
"""
|
|
394
|
+
if execution_id not in self._execution_histories:
|
|
395
|
+
self._execution_histories[execution_id] = {
|
|
396
|
+
"outputs": [],
|
|
397
|
+
"errors": [],
|
|
398
|
+
"drifts": [],
|
|
399
|
+
}
|
|
400
|
+
return self._execution_histories[execution_id]
|
|
401
|
+
|
|
402
|
+
def _record_cycle_output(
|
|
403
|
+
self,
|
|
404
|
+
execution_id: str,
|
|
405
|
+
phase_results: dict[Phase, PhaseResult],
|
|
406
|
+
error_message: str | None = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Record cycle output for stagnation detection.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
execution_id: Execution identifier.
|
|
412
|
+
phase_results: Results from completed phases.
|
|
413
|
+
error_message: Error message if cycle failed.
|
|
414
|
+
"""
|
|
415
|
+
history = self._get_execution_history(execution_id)
|
|
416
|
+
|
|
417
|
+
# Extract output from DELIVER phase (or last completed phase)
|
|
418
|
+
output_text = ""
|
|
419
|
+
for phase in [Phase.DELIVER, Phase.DESIGN, Phase.DEFINE, Phase.DISCOVER]:
|
|
420
|
+
if phase in phase_results:
|
|
421
|
+
output_key = PHASE_PROMPTS[phase.value]["output_key"]
|
|
422
|
+
output_text = str(phase_results[phase].output.get(output_key, ""))
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
history["outputs"].append(output_text)
|
|
426
|
+
|
|
427
|
+
# Keep only last 10 outputs (sliding window)
|
|
428
|
+
if len(history["outputs"]) > 10:
|
|
429
|
+
history["outputs"] = history["outputs"][-10:]
|
|
430
|
+
|
|
431
|
+
if error_message:
|
|
432
|
+
history["errors"].append(error_message)
|
|
433
|
+
if len(history["errors"]) > 10:
|
|
434
|
+
history["errors"] = history["errors"][-10:]
|
|
435
|
+
|
|
436
|
+
def _check_stagnation(
|
|
437
|
+
self,
|
|
438
|
+
execution_id: str,
|
|
439
|
+
seed_id: str,
|
|
440
|
+
iteration: int,
|
|
441
|
+
) -> list[BaseEvent]:
|
|
442
|
+
"""Check for stagnation patterns and emit events.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
execution_id: Execution identifier.
|
|
446
|
+
seed_id: Seed identifier.
|
|
447
|
+
iteration: Current iteration number.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
List of stagnation events (empty if none detected).
|
|
451
|
+
"""
|
|
452
|
+
if not self._enable_stagnation_detection or not self._stagnation_detector:
|
|
453
|
+
return []
|
|
454
|
+
|
|
455
|
+
history_data = self._get_execution_history(execution_id)
|
|
456
|
+
|
|
457
|
+
# Build ExecutionHistory from tracked data
|
|
458
|
+
exec_history = ExecutionHistory.from_lists(
|
|
459
|
+
phase_outputs=history_data["outputs"],
|
|
460
|
+
error_signatures=history_data["errors"],
|
|
461
|
+
drift_scores=history_data["drifts"],
|
|
462
|
+
iteration=iteration,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Run detection
|
|
466
|
+
result = self._stagnation_detector.detect(exec_history)
|
|
467
|
+
|
|
468
|
+
if result.is_err:
|
|
469
|
+
log.warning(
|
|
470
|
+
"execution.stagnation.detection_failed",
|
|
471
|
+
execution_id=execution_id,
|
|
472
|
+
)
|
|
473
|
+
return []
|
|
474
|
+
|
|
475
|
+
# Create events for detected patterns
|
|
476
|
+
events: list[BaseEvent] = []
|
|
477
|
+
for detection in result.value:
|
|
478
|
+
if detection.detected:
|
|
479
|
+
event = create_stagnation_event(
|
|
480
|
+
detection=detection,
|
|
481
|
+
execution_id=execution_id,
|
|
482
|
+
seed_id=seed_id,
|
|
483
|
+
iteration=iteration,
|
|
484
|
+
)
|
|
485
|
+
events.append(event)
|
|
486
|
+
|
|
487
|
+
log.warning(
|
|
488
|
+
"execution.stagnation.pattern_detected",
|
|
489
|
+
execution_id=execution_id,
|
|
490
|
+
pattern=detection.pattern.value,
|
|
491
|
+
confidence=detection.confidence,
|
|
492
|
+
iteration=iteration,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return events
|
|
496
|
+
|
|
497
|
+
def clear_execution_history(self, execution_id: str) -> None:
|
|
498
|
+
"""Clear execution history for an execution.
|
|
499
|
+
|
|
500
|
+
Call this when execution completes successfully or is abandoned.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
execution_id: Execution identifier to clear.
|
|
504
|
+
"""
|
|
505
|
+
if execution_id in self._execution_histories:
|
|
506
|
+
del self._execution_histories[execution_id]
|
|
507
|
+
|
|
508
|
+
def _calculate_backoff(self, attempt: int) -> float:
|
|
509
|
+
"""Calculate exponential backoff delay.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
attempt: The current attempt number (0-indexed).
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Delay in seconds (base_delay * 2^attempt).
|
|
516
|
+
"""
|
|
517
|
+
return float(self._base_delay * (2**attempt))
|
|
518
|
+
|
|
519
|
+
def _emit_event(
|
|
520
|
+
self,
|
|
521
|
+
event_type: str,
|
|
522
|
+
execution_id: str,
|
|
523
|
+
seed_id: str,
|
|
524
|
+
data: dict[str, Any] | None = None,
|
|
525
|
+
) -> BaseEvent:
|
|
526
|
+
"""Emit an execution-related event.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
event_type: Event type (e.g., "execution.cycle.started").
|
|
530
|
+
execution_id: Execution identifier.
|
|
531
|
+
seed_id: Seed identifier.
|
|
532
|
+
data: Additional event data.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
The created event.
|
|
536
|
+
"""
|
|
537
|
+
event_data = {"seed_id": seed_id, **(data or {})}
|
|
538
|
+
return BaseEvent(
|
|
539
|
+
type=event_type,
|
|
540
|
+
aggregate_type="execution",
|
|
541
|
+
aggregate_id=execution_id,
|
|
542
|
+
data=event_data,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _emit_phase_event(
|
|
546
|
+
self,
|
|
547
|
+
event_type: str,
|
|
548
|
+
phase: Phase,
|
|
549
|
+
execution_id: str,
|
|
550
|
+
seed_id: str,
|
|
551
|
+
data: dict[str, Any] | None = None,
|
|
552
|
+
) -> BaseEvent:
|
|
553
|
+
"""Emit a phase-related event.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
event_type: Event type (e.g., "execution.phase.started").
|
|
557
|
+
phase: The current phase.
|
|
558
|
+
execution_id: Execution identifier.
|
|
559
|
+
seed_id: Seed identifier.
|
|
560
|
+
data: Additional event data.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
The created event.
|
|
564
|
+
"""
|
|
565
|
+
event_data = {"phase": phase.value, "seed_id": seed_id, **(data or {})}
|
|
566
|
+
return BaseEvent(
|
|
567
|
+
type=event_type,
|
|
568
|
+
aggregate_type="execution",
|
|
569
|
+
aggregate_id=execution_id,
|
|
570
|
+
data=event_data,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
async def _execute_phase_with_retry(
|
|
574
|
+
self,
|
|
575
|
+
ctx: PhaseContext,
|
|
576
|
+
phase_fn: Any,
|
|
577
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
578
|
+
"""Execute a phase with retry logic.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
ctx: Phase context.
|
|
582
|
+
phase_fn: The async phase function to execute.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
586
|
+
"""
|
|
587
|
+
last_error: Exception | None = None
|
|
588
|
+
|
|
589
|
+
for attempt in range(self._max_retries):
|
|
590
|
+
if attempt > 0:
|
|
591
|
+
delay = self._calculate_backoff(attempt - 1)
|
|
592
|
+
log.info(
|
|
593
|
+
"execution.phase.retry",
|
|
594
|
+
phase=ctx.phase.value,
|
|
595
|
+
attempt=attempt + 1,
|
|
596
|
+
delay_seconds=delay,
|
|
597
|
+
execution_id=ctx.execution_id,
|
|
598
|
+
)
|
|
599
|
+
await asyncio.sleep(delay)
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
result: Result[PhaseResult, ProviderError] = await phase_fn(ctx)
|
|
603
|
+
if result.is_ok:
|
|
604
|
+
return Result.ok(result.value)
|
|
605
|
+
# LLM error - will retry
|
|
606
|
+
last_error = result.error
|
|
607
|
+
log.warning(
|
|
608
|
+
"execution.phase.failed",
|
|
609
|
+
phase=ctx.phase.value,
|
|
610
|
+
attempt=attempt + 1,
|
|
611
|
+
max_retries=self._max_retries,
|
|
612
|
+
error=str(last_error),
|
|
613
|
+
execution_id=ctx.execution_id,
|
|
614
|
+
)
|
|
615
|
+
except Exception as e:
|
|
616
|
+
last_error = e
|
|
617
|
+
log.exception(
|
|
618
|
+
"execution.phase.exception",
|
|
619
|
+
phase=ctx.phase.value,
|
|
620
|
+
attempt=attempt + 1,
|
|
621
|
+
execution_id=ctx.execution_id,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# All retries exhausted
|
|
625
|
+
error_msg = f"Phase {ctx.phase.value} failed after {self._max_retries} attempts"
|
|
626
|
+
log.error(
|
|
627
|
+
"execution.phase.failed.max_retries",
|
|
628
|
+
phase=ctx.phase.value,
|
|
629
|
+
max_retries=self._max_retries,
|
|
630
|
+
last_error=str(last_error),
|
|
631
|
+
execution_id=ctx.execution_id,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return Result.err(
|
|
635
|
+
ExecutionError(
|
|
636
|
+
error_msg,
|
|
637
|
+
phase=ctx.phase,
|
|
638
|
+
attempt=self._max_retries,
|
|
639
|
+
details={"last_error": str(last_error)},
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
async def _call_llm(
|
|
644
|
+
self,
|
|
645
|
+
system_prompt: str,
|
|
646
|
+
user_prompt: str,
|
|
647
|
+
) -> Result[str, ProviderError]:
|
|
648
|
+
"""Call LLM with configured settings.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
system_prompt: System prompt for the LLM.
|
|
652
|
+
user_prompt: User prompt for the LLM.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Result containing LLM response content or ProviderError.
|
|
656
|
+
"""
|
|
657
|
+
from ouroboros.providers.base import CompletionConfig, Message, MessageRole
|
|
658
|
+
|
|
659
|
+
messages = [
|
|
660
|
+
Message(role=MessageRole.SYSTEM, content=system_prompt),
|
|
661
|
+
Message(role=MessageRole.USER, content=user_prompt),
|
|
662
|
+
]
|
|
663
|
+
|
|
664
|
+
config = CompletionConfig(
|
|
665
|
+
model=self._default_model,
|
|
666
|
+
temperature=self._temperature,
|
|
667
|
+
max_tokens=self._max_tokens,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
result = await self._llm_adapter.complete(messages, config)
|
|
671
|
+
if result.is_ok:
|
|
672
|
+
return Result.ok(result.value.content)
|
|
673
|
+
return Result.err(result.error)
|
|
674
|
+
|
|
675
|
+
async def _execute_phase(
|
|
676
|
+
self,
|
|
677
|
+
ctx: PhaseContext,
|
|
678
|
+
phase: Phase,
|
|
679
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
680
|
+
"""Execute a single phase using the template pattern.
|
|
681
|
+
|
|
682
|
+
This method eliminates code duplication by using phase-specific
|
|
683
|
+
prompts from PHASE_PROMPTS configuration.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
ctx: Phase context with execution state.
|
|
687
|
+
phase: The phase to execute.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
691
|
+
"""
|
|
692
|
+
log.info(
|
|
693
|
+
"execution.phase.started",
|
|
694
|
+
phase=phase.value,
|
|
695
|
+
execution_id=ctx.execution_id,
|
|
696
|
+
seed_id=ctx.seed_id,
|
|
697
|
+
iteration=ctx.iteration,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
prompts = PHASE_PROMPTS[phase.value]
|
|
701
|
+
|
|
702
|
+
async def _execute(c: PhaseContext) -> Result[PhaseResult, ProviderError]:
|
|
703
|
+
# Get previous phase output if needed
|
|
704
|
+
previous_output = ""
|
|
705
|
+
if "previous_phase" in prompts:
|
|
706
|
+
prev_phase = Phase(prompts["previous_phase"])
|
|
707
|
+
if prev_phase in c.previous_results:
|
|
708
|
+
previous_output = str(c.previous_results[prev_phase].output)
|
|
709
|
+
|
|
710
|
+
# Format user prompt with context
|
|
711
|
+
user_prompt = prompts["user_template"].format(
|
|
712
|
+
current_ac=c.current_ac,
|
|
713
|
+
execution_id=c.execution_id,
|
|
714
|
+
iteration=c.iteration,
|
|
715
|
+
previous_output=previous_output,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
llm_result = await self._call_llm(prompts["system"], user_prompt)
|
|
719
|
+
if llm_result.is_err:
|
|
720
|
+
return Result.err(llm_result.error)
|
|
721
|
+
|
|
722
|
+
event = self._emit_phase_event(
|
|
723
|
+
"execution.phase.completed",
|
|
724
|
+
phase,
|
|
725
|
+
c.execution_id,
|
|
726
|
+
c.seed_id,
|
|
727
|
+
{prompts["event_data_key"]: True},
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
return Result.ok(
|
|
731
|
+
PhaseResult(
|
|
732
|
+
phase=phase,
|
|
733
|
+
success=True,
|
|
734
|
+
output={prompts["output_key"]: llm_result.value},
|
|
735
|
+
events=[event],
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
return await self._execute_phase_with_retry(ctx, _execute)
|
|
740
|
+
|
|
741
|
+
async def discover(
|
|
742
|
+
self, ctx: PhaseContext
|
|
743
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
744
|
+
"""Execute the Discover phase (diverge).
|
|
745
|
+
|
|
746
|
+
Explores the problem space and gathers insights.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
ctx: Phase context with execution state.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
753
|
+
"""
|
|
754
|
+
return await self._execute_phase(ctx, Phase.DISCOVER)
|
|
755
|
+
|
|
756
|
+
async def define(
|
|
757
|
+
self, ctx: PhaseContext
|
|
758
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
759
|
+
"""Execute the Define phase (converge).
|
|
760
|
+
|
|
761
|
+
Converges on approach, applying ontology filter.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
ctx: Phase context with execution state.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
768
|
+
"""
|
|
769
|
+
return await self._execute_phase(ctx, Phase.DEFINE)
|
|
770
|
+
|
|
771
|
+
async def design(
|
|
772
|
+
self, ctx: PhaseContext
|
|
773
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
774
|
+
"""Execute the Design phase (diverge).
|
|
775
|
+
|
|
776
|
+
Creates solution options.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
ctx: Phase context with execution state.
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
783
|
+
"""
|
|
784
|
+
return await self._execute_phase(ctx, Phase.DESIGN)
|
|
785
|
+
|
|
786
|
+
async def deliver(
|
|
787
|
+
self, ctx: PhaseContext
|
|
788
|
+
) -> Result[PhaseResult, ExecutionError]:
|
|
789
|
+
"""Execute the Deliver phase (converge).
|
|
790
|
+
|
|
791
|
+
Implements and validates the solution.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
ctx: Phase context with execution state.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Result containing PhaseResult on success or ExecutionError on failure.
|
|
798
|
+
"""
|
|
799
|
+
return await self._execute_phase(ctx, Phase.DELIVER)
|
|
800
|
+
|
|
801
|
+
async def run_cycle(
|
|
802
|
+
self,
|
|
803
|
+
execution_id: str,
|
|
804
|
+
seed_id: str,
|
|
805
|
+
current_ac: str,
|
|
806
|
+
iteration: int,
|
|
807
|
+
) -> Result[CycleResult, ExecutionError]:
|
|
808
|
+
"""Run a complete Double Diamond cycle.
|
|
809
|
+
|
|
810
|
+
Executes all four phases in order: Discover → Define → Design → Deliver.
|
|
811
|
+
Emits cycle-level events for full event sourcing traceability.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
execution_id: Unique identifier for this execution.
|
|
815
|
+
seed_id: Identifier of the seed being executed.
|
|
816
|
+
current_ac: The acceptance criterion being worked on.
|
|
817
|
+
iteration: Current iteration number.
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
Result containing CycleResult on success or ExecutionError on failure.
|
|
821
|
+
"""
|
|
822
|
+
log.info(
|
|
823
|
+
"execution.cycle.started",
|
|
824
|
+
execution_id=execution_id,
|
|
825
|
+
seed_id=seed_id,
|
|
826
|
+
iteration=iteration,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
# Emit cycle started event
|
|
830
|
+
cycle_started_event = self._emit_event(
|
|
831
|
+
"execution.cycle.started",
|
|
832
|
+
execution_id,
|
|
833
|
+
seed_id,
|
|
834
|
+
{"iteration": iteration, "current_ac": current_ac},
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
phase_results: dict[Phase, PhaseResult] = {}
|
|
838
|
+
all_events: list[BaseEvent] = [cycle_started_event]
|
|
839
|
+
|
|
840
|
+
# Execute phases in order
|
|
841
|
+
phase_methods = [
|
|
842
|
+
(Phase.DISCOVER, self.discover),
|
|
843
|
+
(Phase.DEFINE, self.define),
|
|
844
|
+
(Phase.DESIGN, self.design),
|
|
845
|
+
(Phase.DELIVER, self.deliver),
|
|
846
|
+
]
|
|
847
|
+
|
|
848
|
+
for phase, method in phase_methods:
|
|
849
|
+
ctx = PhaseContext(
|
|
850
|
+
execution_id=execution_id,
|
|
851
|
+
seed_id=seed_id,
|
|
852
|
+
current_ac=current_ac,
|
|
853
|
+
phase=phase,
|
|
854
|
+
iteration=iteration,
|
|
855
|
+
previous_results=dict(phase_results), # Copy to avoid mutation
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
log.info(
|
|
859
|
+
"execution.phase.transition",
|
|
860
|
+
from_phase=list(phase_results.keys())[-1].value if phase_results else None,
|
|
861
|
+
to_phase=phase.value,
|
|
862
|
+
execution_id=execution_id,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
result = await method(ctx)
|
|
866
|
+
|
|
867
|
+
if result.is_err:
|
|
868
|
+
# Record error for stagnation detection
|
|
869
|
+
self._record_cycle_output(
|
|
870
|
+
execution_id,
|
|
871
|
+
phase_results,
|
|
872
|
+
error_message=str(result.error),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
# Check for stagnation patterns even on failure
|
|
876
|
+
stagnation_events = self._check_stagnation(execution_id, seed_id, iteration)
|
|
877
|
+
all_events.extend(stagnation_events)
|
|
878
|
+
|
|
879
|
+
# Phase failed - emit cycle failed event and return error
|
|
880
|
+
cycle_failed_event = self._emit_event(
|
|
881
|
+
"execution.cycle.failed",
|
|
882
|
+
execution_id,
|
|
883
|
+
seed_id,
|
|
884
|
+
{
|
|
885
|
+
"iteration": iteration,
|
|
886
|
+
"failed_phase": phase.value,
|
|
887
|
+
"error": str(result.error),
|
|
888
|
+
"stagnation_detected": len(stagnation_events) > 0,
|
|
889
|
+
},
|
|
890
|
+
)
|
|
891
|
+
all_events.append(cycle_failed_event)
|
|
892
|
+
|
|
893
|
+
log.error(
|
|
894
|
+
"execution.cycle.failed",
|
|
895
|
+
failed_phase=phase.value,
|
|
896
|
+
execution_id=execution_id,
|
|
897
|
+
error=str(result.error),
|
|
898
|
+
stagnation_patterns_detected=len(stagnation_events),
|
|
899
|
+
)
|
|
900
|
+
return Result.err(result.error)
|
|
901
|
+
|
|
902
|
+
phase_result = result.value
|
|
903
|
+
phase_results[phase] = phase_result
|
|
904
|
+
all_events.extend(phase_result.events)
|
|
905
|
+
|
|
906
|
+
# Record output for stagnation detection
|
|
907
|
+
self._record_cycle_output(execution_id, phase_results)
|
|
908
|
+
|
|
909
|
+
# Check for stagnation patterns (Story 4.1)
|
|
910
|
+
stagnation_events = self._check_stagnation(execution_id, seed_id, iteration)
|
|
911
|
+
all_events.extend(stagnation_events)
|
|
912
|
+
|
|
913
|
+
# Emit cycle completed event
|
|
914
|
+
cycle_completed_event = self._emit_event(
|
|
915
|
+
"execution.cycle.completed",
|
|
916
|
+
execution_id,
|
|
917
|
+
seed_id,
|
|
918
|
+
{
|
|
919
|
+
"iteration": iteration,
|
|
920
|
+
"phases_completed": len(phase_results),
|
|
921
|
+
"stagnation_detected": len(stagnation_events) > 0,
|
|
922
|
+
},
|
|
923
|
+
)
|
|
924
|
+
all_events.append(cycle_completed_event)
|
|
925
|
+
|
|
926
|
+
log.info(
|
|
927
|
+
"execution.cycle.completed",
|
|
928
|
+
execution_id=execution_id,
|
|
929
|
+
seed_id=seed_id,
|
|
930
|
+
iteration=iteration,
|
|
931
|
+
phases_completed=len(phase_results),
|
|
932
|
+
stagnation_patterns_detected=len(stagnation_events),
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
return Result.ok(
|
|
936
|
+
CycleResult(
|
|
937
|
+
execution_id=execution_id,
|
|
938
|
+
seed_id=seed_id,
|
|
939
|
+
current_ac=current_ac,
|
|
940
|
+
success=True,
|
|
941
|
+
phase_results=phase_results,
|
|
942
|
+
events=all_events,
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
async def run_cycle_with_decomposition(
|
|
947
|
+
self,
|
|
948
|
+
execution_id: str,
|
|
949
|
+
seed_id: str,
|
|
950
|
+
current_ac: str,
|
|
951
|
+
iteration: int,
|
|
952
|
+
*,
|
|
953
|
+
depth: int = 0,
|
|
954
|
+
max_depth: int = 5,
|
|
955
|
+
parent_ac: str | None = None,
|
|
956
|
+
) -> Result[CycleResult, ExecutionError]:
|
|
957
|
+
"""Run Double Diamond cycle with hierarchical AC decomposition.
|
|
958
|
+
|
|
959
|
+
This method extends run_cycle to support:
|
|
960
|
+
- Atomicity detection at Define phase
|
|
961
|
+
- Recursive decomposition up to max_depth
|
|
962
|
+
- Context compression at depth 3+
|
|
963
|
+
|
|
964
|
+
If the AC is atomic, executes the full cycle (Discover → Define → Design → Deliver).
|
|
965
|
+
If non-atomic, decomposes after Define and recursively processes children.
|
|
966
|
+
|
|
967
|
+
Args:
|
|
968
|
+
execution_id: Unique identifier for this execution.
|
|
969
|
+
seed_id: Identifier of the seed being executed.
|
|
970
|
+
current_ac: The acceptance criterion being worked on.
|
|
971
|
+
iteration: Current iteration number.
|
|
972
|
+
depth: Current depth in AC tree (0 = root).
|
|
973
|
+
max_depth: Maximum decomposition depth (default: 5).
|
|
974
|
+
parent_ac: Parent AC content if this is a child AC.
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
Result containing CycleResult on success or ExecutionError on failure.
|
|
978
|
+
"""
|
|
979
|
+
from ouroboros.events.decomposition import (
|
|
980
|
+
create_ac_atomicity_checked_event,
|
|
981
|
+
create_ac_marked_atomic_event,
|
|
982
|
+
)
|
|
983
|
+
from ouroboros.execution.atomicity import AtomicityCriteria, check_atomicity
|
|
984
|
+
from ouroboros.execution.decomposition import decompose_ac
|
|
985
|
+
|
|
986
|
+
log.info(
|
|
987
|
+
"execution.cycle_with_decomposition.started",
|
|
988
|
+
execution_id=execution_id,
|
|
989
|
+
seed_id=seed_id,
|
|
990
|
+
depth=depth,
|
|
991
|
+
max_depth=max_depth,
|
|
992
|
+
iteration=iteration,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Check max depth - if reached, force execution without further decomposition
|
|
996
|
+
if depth >= max_depth:
|
|
997
|
+
log.warning(
|
|
998
|
+
"execution.max_depth_reached",
|
|
999
|
+
execution_id=execution_id,
|
|
1000
|
+
depth=depth,
|
|
1001
|
+
max_depth=max_depth,
|
|
1002
|
+
)
|
|
1003
|
+
# Execute normally without decomposition
|
|
1004
|
+
return await self.run_cycle(execution_id, seed_id, current_ac, iteration)
|
|
1005
|
+
|
|
1006
|
+
# Emit cycle started event
|
|
1007
|
+
cycle_started_event = self._emit_event(
|
|
1008
|
+
"execution.cycle.started",
|
|
1009
|
+
execution_id,
|
|
1010
|
+
seed_id,
|
|
1011
|
+
{"iteration": iteration, "current_ac": current_ac, "depth": depth},
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
phase_results: dict[Phase, PhaseResult] = {}
|
|
1015
|
+
all_events: list[BaseEvent] = [cycle_started_event]
|
|
1016
|
+
|
|
1017
|
+
# Phase 1: DISCOVER
|
|
1018
|
+
discover_ctx = PhaseContext(
|
|
1019
|
+
execution_id=execution_id,
|
|
1020
|
+
seed_id=seed_id,
|
|
1021
|
+
current_ac=current_ac,
|
|
1022
|
+
phase=Phase.DISCOVER,
|
|
1023
|
+
iteration=iteration,
|
|
1024
|
+
previous_results={},
|
|
1025
|
+
depth=depth,
|
|
1026
|
+
parent_ac=parent_ac,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
discover_result = await self.discover(discover_ctx)
|
|
1030
|
+
if discover_result.is_err:
|
|
1031
|
+
cycle_failed_event = self._emit_event(
|
|
1032
|
+
"execution.cycle.failed",
|
|
1033
|
+
execution_id,
|
|
1034
|
+
seed_id,
|
|
1035
|
+
{"iteration": iteration, "failed_phase": "discover", "error": str(discover_result.error)},
|
|
1036
|
+
)
|
|
1037
|
+
all_events.append(cycle_failed_event)
|
|
1038
|
+
return Result.err(discover_result.error)
|
|
1039
|
+
|
|
1040
|
+
phase_results[Phase.DISCOVER] = discover_result.value
|
|
1041
|
+
all_events.extend(discover_result.value.events)
|
|
1042
|
+
|
|
1043
|
+
# Phase 2: DEFINE
|
|
1044
|
+
define_ctx = PhaseContext(
|
|
1045
|
+
execution_id=execution_id,
|
|
1046
|
+
seed_id=seed_id,
|
|
1047
|
+
current_ac=current_ac,
|
|
1048
|
+
phase=Phase.DEFINE,
|
|
1049
|
+
iteration=iteration,
|
|
1050
|
+
previous_results=dict(phase_results),
|
|
1051
|
+
depth=depth,
|
|
1052
|
+
parent_ac=parent_ac,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
define_result = await self.define(define_ctx)
|
|
1056
|
+
if define_result.is_err:
|
|
1057
|
+
cycle_failed_event = self._emit_event(
|
|
1058
|
+
"execution.cycle.failed",
|
|
1059
|
+
execution_id,
|
|
1060
|
+
seed_id,
|
|
1061
|
+
{"iteration": iteration, "failed_phase": "define", "error": str(define_result.error)},
|
|
1062
|
+
)
|
|
1063
|
+
all_events.append(cycle_failed_event)
|
|
1064
|
+
return Result.err(define_result.error)
|
|
1065
|
+
|
|
1066
|
+
phase_results[Phase.DEFINE] = define_result.value
|
|
1067
|
+
all_events.extend(define_result.value.events)
|
|
1068
|
+
|
|
1069
|
+
# After Define: Check atomicity
|
|
1070
|
+
atomicity_result = await check_atomicity(
|
|
1071
|
+
ac_content=current_ac,
|
|
1072
|
+
llm_adapter=self._llm_adapter,
|
|
1073
|
+
criteria=AtomicityCriteria(),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
is_atomic = True # Default to atomic if check fails
|
|
1077
|
+
if atomicity_result.is_ok:
|
|
1078
|
+
atomicity = atomicity_result.value
|
|
1079
|
+
is_atomic = atomicity.is_atomic
|
|
1080
|
+
|
|
1081
|
+
# Emit atomicity checked event
|
|
1082
|
+
atomicity_event = create_ac_atomicity_checked_event(
|
|
1083
|
+
ac_id=execution_id,
|
|
1084
|
+
execution_id=execution_id,
|
|
1085
|
+
is_atomic=atomicity.is_atomic,
|
|
1086
|
+
complexity_score=atomicity.complexity_score,
|
|
1087
|
+
tool_count=atomicity.tool_count,
|
|
1088
|
+
estimated_duration=atomicity.estimated_duration,
|
|
1089
|
+
reasoning=atomicity.reasoning,
|
|
1090
|
+
)
|
|
1091
|
+
all_events.append(atomicity_event)
|
|
1092
|
+
|
|
1093
|
+
log.info(
|
|
1094
|
+
"execution.atomicity.checked",
|
|
1095
|
+
execution_id=execution_id,
|
|
1096
|
+
is_atomic=is_atomic,
|
|
1097
|
+
complexity=atomicity.complexity_score,
|
|
1098
|
+
)
|
|
1099
|
+
else:
|
|
1100
|
+
log.warning(
|
|
1101
|
+
"execution.atomicity.check_failed",
|
|
1102
|
+
execution_id=execution_id,
|
|
1103
|
+
error=str(atomicity_result.error),
|
|
1104
|
+
defaulting_to_atomic=True,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
# If non-atomic and not at max depth, decompose
|
|
1108
|
+
if not is_atomic and depth < max_depth - 1:
|
|
1109
|
+
# Get discover insights for decomposition
|
|
1110
|
+
discover_insights = str(phase_results[Phase.DISCOVER].output.get("insights", ""))
|
|
1111
|
+
|
|
1112
|
+
decompose_result = await decompose_ac(
|
|
1113
|
+
ac_content=current_ac,
|
|
1114
|
+
ac_id=execution_id,
|
|
1115
|
+
execution_id=execution_id,
|
|
1116
|
+
depth=depth,
|
|
1117
|
+
llm_adapter=self._llm_adapter,
|
|
1118
|
+
discover_insights=discover_insights,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
if decompose_result.is_ok:
|
|
1122
|
+
decomposition = decompose_result.value
|
|
1123
|
+
all_events.extend(decomposition.events)
|
|
1124
|
+
|
|
1125
|
+
log.info(
|
|
1126
|
+
"execution.decomposition.completed",
|
|
1127
|
+
execution_id=execution_id,
|
|
1128
|
+
child_count=len(decomposition.child_acs),
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
# Execute children recursively with SubAgent isolation
|
|
1132
|
+
# Import SubAgent utilities for isolation (AC 4, 5)
|
|
1133
|
+
from ouroboros.execution.subagent import (
|
|
1134
|
+
create_subagent_completed_event,
|
|
1135
|
+
create_subagent_failed_event,
|
|
1136
|
+
create_subagent_started_event,
|
|
1137
|
+
create_subagent_validated_event,
|
|
1138
|
+
validate_child_result,
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
child_results: list[CycleResult] = []
|
|
1142
|
+
for idx, (child_ac, child_id) in enumerate(
|
|
1143
|
+
zip(decomposition.child_acs, decomposition.child_ac_ids, strict=True)
|
|
1144
|
+
):
|
|
1145
|
+
child_exec_id = f"{execution_id}_child_{idx}"
|
|
1146
|
+
|
|
1147
|
+
# Emit SubAgent started event
|
|
1148
|
+
subagent_started_event = create_subagent_started_event(
|
|
1149
|
+
subagent_id=child_exec_id,
|
|
1150
|
+
parent_execution_id=execution_id,
|
|
1151
|
+
child_ac=child_ac,
|
|
1152
|
+
depth=depth + 1,
|
|
1153
|
+
)
|
|
1154
|
+
all_events.append(subagent_started_event)
|
|
1155
|
+
|
|
1156
|
+
log.info(
|
|
1157
|
+
"execution.subagent.started",
|
|
1158
|
+
subagent_id=child_exec_id,
|
|
1159
|
+
parent_execution_id=execution_id,
|
|
1160
|
+
depth=depth + 1,
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
# Execute child in isolated context
|
|
1164
|
+
# Note: FilteredContext is available via create_filtered_context()
|
|
1165
|
+
# but DoubleDiamond doesn't maintain WorkflowContext state.
|
|
1166
|
+
# The isolation is achieved by:
|
|
1167
|
+
# 1. Passing only child_ac (not parent context)
|
|
1168
|
+
# 2. Using immutable CycleResult
|
|
1169
|
+
# 3. Validating results before integration
|
|
1170
|
+
child_result = await self.run_cycle_with_decomposition(
|
|
1171
|
+
execution_id=child_exec_id,
|
|
1172
|
+
seed_id=seed_id,
|
|
1173
|
+
current_ac=child_ac,
|
|
1174
|
+
iteration=1,
|
|
1175
|
+
depth=depth + 1,
|
|
1176
|
+
max_depth=max_depth,
|
|
1177
|
+
parent_ac=current_ac,
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
if child_result.is_ok:
|
|
1181
|
+
# AC 4: Validate child result before integration
|
|
1182
|
+
validation_result = validate_child_result(
|
|
1183
|
+
child_result.value, child_ac
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
if validation_result.is_ok:
|
|
1187
|
+
validated_child = validation_result.value
|
|
1188
|
+
child_results.append(validated_child)
|
|
1189
|
+
all_events.extend(validated_child.events)
|
|
1190
|
+
|
|
1191
|
+
# Emit validation success event
|
|
1192
|
+
validated_event = create_subagent_validated_event(
|
|
1193
|
+
subagent_id=child_exec_id,
|
|
1194
|
+
parent_execution_id=execution_id,
|
|
1195
|
+
validation_passed=True,
|
|
1196
|
+
)
|
|
1197
|
+
all_events.append(validated_event)
|
|
1198
|
+
|
|
1199
|
+
# Emit SubAgent completed event
|
|
1200
|
+
completed_event = create_subagent_completed_event(
|
|
1201
|
+
subagent_id=child_exec_id,
|
|
1202
|
+
parent_execution_id=execution_id,
|
|
1203
|
+
success=True,
|
|
1204
|
+
child_count=len(validated_child.child_results),
|
|
1205
|
+
)
|
|
1206
|
+
all_events.append(completed_event)
|
|
1207
|
+
|
|
1208
|
+
log.info(
|
|
1209
|
+
"execution.subagent.completed",
|
|
1210
|
+
subagent_id=child_exec_id,
|
|
1211
|
+
success=True,
|
|
1212
|
+
)
|
|
1213
|
+
else:
|
|
1214
|
+
# Validation failed - log but don't crash (AC 5)
|
|
1215
|
+
validation_error = validation_result.error
|
|
1216
|
+
log.warning(
|
|
1217
|
+
"execution.subagent.validation_failed",
|
|
1218
|
+
subagent_id=child_exec_id,
|
|
1219
|
+
error=str(validation_error),
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
# Emit validation failure event
|
|
1223
|
+
validated_event = create_subagent_validated_event(
|
|
1224
|
+
subagent_id=child_exec_id,
|
|
1225
|
+
parent_execution_id=execution_id,
|
|
1226
|
+
validation_passed=False,
|
|
1227
|
+
validation_message=str(validation_error),
|
|
1228
|
+
)
|
|
1229
|
+
all_events.append(validated_event)
|
|
1230
|
+
|
|
1231
|
+
# Emit failed event
|
|
1232
|
+
failed_event = create_subagent_failed_event(
|
|
1233
|
+
subagent_id=child_exec_id,
|
|
1234
|
+
parent_execution_id=execution_id,
|
|
1235
|
+
error_message=f"Validation failed: {validation_error}",
|
|
1236
|
+
is_retriable=False,
|
|
1237
|
+
)
|
|
1238
|
+
all_events.append(failed_event)
|
|
1239
|
+
# Continue with other children (resilience - AC 5)
|
|
1240
|
+
else:
|
|
1241
|
+
# AC 5: Failed SubAgent doesn't crash parent
|
|
1242
|
+
log.error(
|
|
1243
|
+
"execution.subagent.failed",
|
|
1244
|
+
parent_execution_id=execution_id,
|
|
1245
|
+
child_execution_id=child_exec_id,
|
|
1246
|
+
error=str(child_result.error),
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
# Emit SubAgent failed event
|
|
1250
|
+
failed_event = create_subagent_failed_event(
|
|
1251
|
+
subagent_id=child_exec_id,
|
|
1252
|
+
parent_execution_id=execution_id,
|
|
1253
|
+
error_message=str(child_result.error),
|
|
1254
|
+
is_retriable=getattr(child_result.error, "is_retriable", False),
|
|
1255
|
+
)
|
|
1256
|
+
all_events.append(failed_event)
|
|
1257
|
+
# Continue with other children (resilience - AC 5)
|
|
1258
|
+
|
|
1259
|
+
# Emit cycle completed event (decomposed)
|
|
1260
|
+
cycle_completed_event = self._emit_event(
|
|
1261
|
+
"execution.cycle.completed",
|
|
1262
|
+
execution_id,
|
|
1263
|
+
seed_id,
|
|
1264
|
+
{
|
|
1265
|
+
"iteration": iteration,
|
|
1266
|
+
"phases_completed": 2, # Discover + Define
|
|
1267
|
+
"decomposed": True,
|
|
1268
|
+
"child_count": len(child_results),
|
|
1269
|
+
},
|
|
1270
|
+
)
|
|
1271
|
+
all_events.append(cycle_completed_event)
|
|
1272
|
+
|
|
1273
|
+
return Result.ok(
|
|
1274
|
+
CycleResult(
|
|
1275
|
+
execution_id=execution_id,
|
|
1276
|
+
seed_id=seed_id,
|
|
1277
|
+
current_ac=current_ac,
|
|
1278
|
+
success=True,
|
|
1279
|
+
phase_results=phase_results,
|
|
1280
|
+
events=all_events,
|
|
1281
|
+
is_decomposed=True,
|
|
1282
|
+
child_results=tuple(child_results),
|
|
1283
|
+
depth=depth,
|
|
1284
|
+
)
|
|
1285
|
+
)
|
|
1286
|
+
else:
|
|
1287
|
+
log.warning(
|
|
1288
|
+
"execution.decomposition.failed",
|
|
1289
|
+
execution_id=execution_id,
|
|
1290
|
+
error=str(decompose_result.error),
|
|
1291
|
+
continuing_as_atomic=True,
|
|
1292
|
+
)
|
|
1293
|
+
# Fall through to execute as atomic
|
|
1294
|
+
|
|
1295
|
+
# Atomic path: emit marked atomic event
|
|
1296
|
+
atomic_event = create_ac_marked_atomic_event(
|
|
1297
|
+
ac_id=execution_id,
|
|
1298
|
+
execution_id=execution_id,
|
|
1299
|
+
depth=depth,
|
|
1300
|
+
)
|
|
1301
|
+
all_events.append(atomic_event)
|
|
1302
|
+
|
|
1303
|
+
# Continue with Design and Deliver phases
|
|
1304
|
+
# Phase 3: DESIGN
|
|
1305
|
+
design_ctx = PhaseContext(
|
|
1306
|
+
execution_id=execution_id,
|
|
1307
|
+
seed_id=seed_id,
|
|
1308
|
+
current_ac=current_ac,
|
|
1309
|
+
phase=Phase.DESIGN,
|
|
1310
|
+
iteration=iteration,
|
|
1311
|
+
previous_results=dict(phase_results),
|
|
1312
|
+
depth=depth,
|
|
1313
|
+
parent_ac=parent_ac,
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
design_result = await self.design(design_ctx)
|
|
1317
|
+
if design_result.is_err:
|
|
1318
|
+
cycle_failed_event = self._emit_event(
|
|
1319
|
+
"execution.cycle.failed",
|
|
1320
|
+
execution_id,
|
|
1321
|
+
seed_id,
|
|
1322
|
+
{"iteration": iteration, "failed_phase": "design", "error": str(design_result.error)},
|
|
1323
|
+
)
|
|
1324
|
+
all_events.append(cycle_failed_event)
|
|
1325
|
+
return Result.err(design_result.error)
|
|
1326
|
+
|
|
1327
|
+
phase_results[Phase.DESIGN] = design_result.value
|
|
1328
|
+
all_events.extend(design_result.value.events)
|
|
1329
|
+
|
|
1330
|
+
# Phase 4: DELIVER
|
|
1331
|
+
deliver_ctx = PhaseContext(
|
|
1332
|
+
execution_id=execution_id,
|
|
1333
|
+
seed_id=seed_id,
|
|
1334
|
+
current_ac=current_ac,
|
|
1335
|
+
phase=Phase.DELIVER,
|
|
1336
|
+
iteration=iteration,
|
|
1337
|
+
previous_results=dict(phase_results),
|
|
1338
|
+
depth=depth,
|
|
1339
|
+
parent_ac=parent_ac,
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
deliver_result = await self.deliver(deliver_ctx)
|
|
1343
|
+
if deliver_result.is_err:
|
|
1344
|
+
cycle_failed_event = self._emit_event(
|
|
1345
|
+
"execution.cycle.failed",
|
|
1346
|
+
execution_id,
|
|
1347
|
+
seed_id,
|
|
1348
|
+
{"iteration": iteration, "failed_phase": "deliver", "error": str(deliver_result.error)},
|
|
1349
|
+
)
|
|
1350
|
+
all_events.append(cycle_failed_event)
|
|
1351
|
+
return Result.err(deliver_result.error)
|
|
1352
|
+
|
|
1353
|
+
phase_results[Phase.DELIVER] = deliver_result.value
|
|
1354
|
+
all_events.extend(deliver_result.value.events)
|
|
1355
|
+
|
|
1356
|
+
# Emit cycle completed event (atomic)
|
|
1357
|
+
cycle_completed_event = self._emit_event(
|
|
1358
|
+
"execution.cycle.completed",
|
|
1359
|
+
execution_id,
|
|
1360
|
+
seed_id,
|
|
1361
|
+
{"iteration": iteration, "phases_completed": 4, "decomposed": False},
|
|
1362
|
+
)
|
|
1363
|
+
all_events.append(cycle_completed_event)
|
|
1364
|
+
|
|
1365
|
+
log.info(
|
|
1366
|
+
"execution.cycle_with_decomposition.completed",
|
|
1367
|
+
execution_id=execution_id,
|
|
1368
|
+
seed_id=seed_id,
|
|
1369
|
+
depth=depth,
|
|
1370
|
+
is_atomic=True,
|
|
1371
|
+
phases_completed=4,
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
return Result.ok(
|
|
1375
|
+
CycleResult(
|
|
1376
|
+
execution_id=execution_id,
|
|
1377
|
+
seed_id=seed_id,
|
|
1378
|
+
current_ac=current_ac,
|
|
1379
|
+
success=True,
|
|
1380
|
+
phase_results=phase_results,
|
|
1381
|
+
events=all_events,
|
|
1382
|
+
is_decomposed=False,
|
|
1383
|
+
child_results=(),
|
|
1384
|
+
depth=depth,
|
|
1385
|
+
)
|
|
1386
|
+
)
|