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,595 @@
1
+ """Lateral thinking personas for stagnation recovery.
2
+
3
+ This module implements Story 4.2: Lateral Thinking Personas.
4
+
5
+ Provides 5 thinking personas to break through stagnation:
6
+ 1. Hacker: Unconventional, finds workarounds
7
+ 2. Researcher: Seeks additional information
8
+ 3. Simplifier: Reduces complexity, removes assumptions
9
+ 4. Architect: Restructures the approach fundamentally
10
+ 5. Contrarian: Challenges assumptions, inverts the problem
11
+
12
+ Design:
13
+ - Stateless thinking: Personas generate prompts, not solutions
14
+ - Pattern-aware: Selection hints based on stagnation type
15
+ - Event emission: Each persona activation emits events
16
+
17
+ Usage:
18
+ from ouroboros.resilience.lateral import (
19
+ LateralThinker,
20
+ ThinkingPersona,
21
+ )
22
+
23
+ thinker = LateralThinker()
24
+ result = thinker.generate_alternative(
25
+ persona=ThinkingPersona.HACKER,
26
+ problem_context="Failing to parse XML",
27
+ current_approach="Using regex to parse",
28
+ )
29
+ print(result.value.prompt) # Get the alternative thinking prompt
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from dataclasses import dataclass, field
35
+ from enum import Enum
36
+ from typing import Any
37
+
38
+ from ouroboros.core.types import Result
39
+ from ouroboros.events.base import BaseEvent
40
+ from ouroboros.observability.logging import get_logger
41
+ from ouroboros.resilience.stagnation import StagnationPattern
42
+
43
+ log = get_logger(__name__)
44
+
45
+
46
+ # =============================================================================
47
+ # Enums and Data Models
48
+ # =============================================================================
49
+
50
+
51
+ class ThinkingPersona(str, Enum):
52
+ """Five lateral thinking personas for breaking through stagnation.
53
+
54
+ Each persona approaches problems from a fundamentally different angle,
55
+ providing diverse strategies for escaping stuck states.
56
+
57
+ Attributes:
58
+ HACKER: Unconventional, bypasses obstacles, finds workarounds
59
+ RESEARCHER: Seeks more information, explores context
60
+ SIMPLIFIER: Reduces complexity, challenges assumptions
61
+ ARCHITECT: Restructures fundamentally, changes perspective
62
+ CONTRARIAN: Inverts assumptions, questions everything
63
+ """
64
+
65
+ HACKER = "hacker"
66
+ RESEARCHER = "researcher"
67
+ SIMPLIFIER = "simplifier"
68
+ ARCHITECT = "architect"
69
+ CONTRARIAN = "contrarian"
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ """Return human-readable description of persona."""
74
+ descriptions = {
75
+ ThinkingPersona.HACKER: "Finds unconventional workarounds",
76
+ ThinkingPersona.RESEARCHER: "Seeks additional information",
77
+ ThinkingPersona.SIMPLIFIER: "Reduces complexity",
78
+ ThinkingPersona.ARCHITECT: "Restructures the approach",
79
+ ThinkingPersona.CONTRARIAN: "Challenges assumptions",
80
+ }
81
+ return descriptions[self]
82
+
83
+ @property
84
+ def affinity_patterns(self) -> tuple[StagnationPattern, ...]:
85
+ """Return stagnation patterns this persona handles well.
86
+
87
+ Each persona has affinity for certain stagnation patterns:
88
+ - HACKER: Good for Spinning (same error repeated)
89
+ - RESEARCHER: Good for No Drift (needs more info)
90
+ - SIMPLIFIER: Good for Diminishing Returns (overcomplicated)
91
+ - ARCHITECT: Good for Oscillation (structural problem)
92
+ - CONTRARIAN: Good for all patterns (challenges everything)
93
+ """
94
+ affinities: dict[ThinkingPersona, tuple[StagnationPattern, ...]] = {
95
+ ThinkingPersona.HACKER: (StagnationPattern.SPINNING,),
96
+ ThinkingPersona.RESEARCHER: (
97
+ StagnationPattern.NO_DRIFT,
98
+ StagnationPattern.DIMINISHING_RETURNS,
99
+ ),
100
+ ThinkingPersona.SIMPLIFIER: (
101
+ StagnationPattern.DIMINISHING_RETURNS,
102
+ StagnationPattern.OSCILLATION,
103
+ ),
104
+ ThinkingPersona.ARCHITECT: (
105
+ StagnationPattern.OSCILLATION,
106
+ StagnationPattern.NO_DRIFT,
107
+ ),
108
+ ThinkingPersona.CONTRARIAN: (
109
+ StagnationPattern.SPINNING,
110
+ StagnationPattern.OSCILLATION,
111
+ StagnationPattern.NO_DRIFT,
112
+ StagnationPattern.DIMINISHING_RETURNS,
113
+ ),
114
+ }
115
+ return affinities[self]
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class PersonaStrategy:
120
+ """Strategy configuration for a thinking persona.
121
+
122
+ Describes how a persona approaches problem-solving.
123
+
124
+ Attributes:
125
+ persona: The thinking persona.
126
+ system_prompt: System-level prompt defining persona behavior.
127
+ approach_instructions: Step-by-step thinking instructions.
128
+ question_templates: Templates for probing questions.
129
+ """
130
+
131
+ persona: ThinkingPersona
132
+ system_prompt: str
133
+ approach_instructions: tuple[str, ...]
134
+ question_templates: tuple[str, ...] = field(default_factory=tuple)
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class LateralThinkingResult:
139
+ """Result of applying lateral thinking to a problem.
140
+
141
+ Attributes:
142
+ persona: The persona that generated this result.
143
+ prompt: Complete prompt for LLM to think laterally.
144
+ approach_summary: Brief summary of the thinking approach.
145
+ questions: Probing questions to consider.
146
+ """
147
+
148
+ persona: ThinkingPersona
149
+ prompt: str
150
+ approach_summary: str
151
+ questions: tuple[str, ...] = field(default_factory=tuple)
152
+
153
+
154
+ # =============================================================================
155
+ # Persona Strategies
156
+ # =============================================================================
157
+
158
+ _PERSONA_STRATEGIES: dict[ThinkingPersona, PersonaStrategy] = {
159
+ ThinkingPersona.HACKER: PersonaStrategy(
160
+ persona=ThinkingPersona.HACKER,
161
+ system_prompt=(
162
+ "You are a creative problem-solver who finds unconventional workarounds. "
163
+ "You don't accept 'impossible' - you find the path others miss. "
164
+ "Rules are obstacles to route around, not walls to stop at. "
165
+ "Think like a security researcher finding exploits in assumptions."
166
+ ),
167
+ approach_instructions=(
168
+ "1. Identify the explicit and implicit constraints being followed",
169
+ "2. Question each constraint - which ones are actually required?",
170
+ "3. Look for edge cases, corner cases, or boundary conditions",
171
+ "4. Consider bypassing the problem entirely - solve a different problem",
172
+ "5. What would a malicious actor do? Use that creativity constructively",
173
+ ),
174
+ question_templates=(
175
+ "What assumptions are we making that might not be true?",
176
+ "What would happen if we bypassed {obstacle} entirely?",
177
+ "Is there a simpler problem we could solve instead?",
178
+ "What would break if we did the 'wrong' thing here?",
179
+ ),
180
+ ),
181
+ ThinkingPersona.RESEARCHER: PersonaStrategy(
182
+ persona=ThinkingPersona.RESEARCHER,
183
+ system_prompt=(
184
+ "You are a thorough researcher who believes every problem can be solved "
185
+ "with enough information. You dig deep into documentation, examples, "
186
+ "and prior art. You never assume - you verify. Your strength is finding "
187
+ "the missing context that unlocks the solution."
188
+ ),
189
+ approach_instructions=(
190
+ "1. Identify what information is missing or uncertain",
191
+ "2. List all assumptions being made without verification",
192
+ "3. Research similar problems and their solutions",
193
+ "4. Look for official documentation or authoritative sources",
194
+ "5. Consider what an expert in this domain would know",
195
+ ),
196
+ question_templates=(
197
+ "What documentation have we not consulted?",
198
+ "Has anyone solved a similar problem before?",
199
+ "What would an expert in {domain} ask first?",
200
+ "What information are we assuming but haven't verified?",
201
+ ),
202
+ ),
203
+ ThinkingPersona.SIMPLIFIER: PersonaStrategy(
204
+ persona=ThinkingPersona.SIMPLIFIER,
205
+ system_prompt=(
206
+ "You believe complexity is the enemy of progress. Every requirement "
207
+ "should be questioned, every abstraction justified. You find the "
208
+ "minimal viable solution. You remove, you reduce, you simplify until "
209
+ "only the essential remains."
210
+ ),
211
+ approach_instructions=(
212
+ "1. List every component and requirement involved",
213
+ "2. Challenge each one - is it truly necessary?",
214
+ "3. Identify the absolute minimum needed to solve the core problem",
215
+ "4. Remove abstractions and solve concretely first",
216
+ "5. Ask: what's the simplest thing that could possibly work?",
217
+ ),
218
+ question_templates=(
219
+ "What can we remove without losing the core value?",
220
+ "Is this complexity earning its keep?",
221
+ "What's the simplest version of this that would work?",
222
+ "Are we solving the problem or building a framework?",
223
+ ),
224
+ ),
225
+ ThinkingPersona.ARCHITECT: PersonaStrategy(
226
+ persona=ThinkingPersona.ARCHITECT,
227
+ system_prompt=(
228
+ "You see problems as structural, not just tactical. When something "
229
+ "doesn't work, you don't just fix the symptom - you redesign the "
230
+ "foundation. You think in patterns, abstractions, and systems. "
231
+ "Your solutions prevent future problems, not just solve current ones."
232
+ ),
233
+ approach_instructions=(
234
+ "1. Map the current structure and its dependencies",
235
+ "2. Identify structural mismatches or coupling issues",
236
+ "3. Consider alternative architectures that avoid the problem",
237
+ "4. Think about what data structures would make this trivial",
238
+ "5. Design from first principles - what's the ideal structure?",
239
+ ),
240
+ question_templates=(
241
+ "What if we structured this completely differently?",
242
+ "Is the problem in our approach or our architecture?",
243
+ "What data structure would make this problem disappear?",
244
+ "Are we fighting the current design instead of changing it?",
245
+ ),
246
+ ),
247
+ ThinkingPersona.CONTRARIAN: PersonaStrategy(
248
+ persona=ThinkingPersona.CONTRARIAN,
249
+ system_prompt=(
250
+ "You question everything. What everyone assumes is true, you examine. "
251
+ "What seems obviously correct, you invert. You're not contrarian to be "
252
+ "difficult - you're contrarian because real innovation comes from "
253
+ "questioning the unquestionable. The opposite of a great truth is "
254
+ "often another great truth."
255
+ ),
256
+ approach_instructions=(
257
+ "1. List every assumption being made",
258
+ "2. For each assumption, consider its opposite",
259
+ "3. What if the 'problem' is actually the solution?",
260
+ "4. What if we're solving the wrong problem entirely?",
261
+ "5. Consider the opposite of the 'obvious' approach",
262
+ ),
263
+ question_templates=(
264
+ "What if the opposite of our assumption is true?",
265
+ "What if what we're trying to prevent should actually happen?",
266
+ "Are we solving the right problem?",
267
+ "What would happen if we did nothing?",
268
+ ),
269
+ ),
270
+ }
271
+
272
+
273
+ # =============================================================================
274
+ # Lateral Thinker
275
+ # =============================================================================
276
+
277
+
278
+ class LateralThinker:
279
+ """Generates alternative thinking approaches using personas.
280
+
281
+ Stateless generator that creates prompts for LLM to think laterally.
282
+ Each persona provides a different perspective on the problem.
283
+
284
+ Attributes:
285
+ strategies: Mapping of personas to their strategies.
286
+ """
287
+
288
+ def __init__(
289
+ self,
290
+ *,
291
+ custom_strategies: dict[ThinkingPersona, PersonaStrategy] | None = None,
292
+ ) -> None:
293
+ """Initialize LateralThinker with optional custom strategies.
294
+
295
+ Args:
296
+ custom_strategies: Optional overrides for persona strategies.
297
+ """
298
+ self._strategies = {**_PERSONA_STRATEGIES}
299
+ if custom_strategies:
300
+ self._strategies.update(custom_strategies)
301
+
302
+ def get_strategy(self, persona: ThinkingPersona) -> PersonaStrategy:
303
+ """Get the strategy for a specific persona.
304
+
305
+ Args:
306
+ persona: The thinking persona.
307
+
308
+ Returns:
309
+ PersonaStrategy for the given persona.
310
+ """
311
+ return self._strategies[persona]
312
+
313
+ def generate_alternative(
314
+ self,
315
+ persona: ThinkingPersona,
316
+ problem_context: str,
317
+ current_approach: str,
318
+ *,
319
+ failed_attempts: tuple[str, ...] = (),
320
+ ) -> Result[LateralThinkingResult, str]:
321
+ """Generate an alternative thinking approach using a persona.
322
+
323
+ Combines persona strategy with problem context to create a prompt
324
+ that guides LLM thinking from a different perspective.
325
+
326
+ Args:
327
+ persona: The thinking persona to use.
328
+ problem_context: Description of the problem.
329
+ current_approach: What has been tried so far.
330
+ failed_attempts: Previous approaches that failed.
331
+
332
+ Returns:
333
+ Result containing LateralThinkingResult or error message.
334
+ """
335
+ log.debug(
336
+ "resilience.lateral.generating",
337
+ persona=persona.value,
338
+ problem_length=len(problem_context),
339
+ approach_length=len(current_approach),
340
+ failed_count=len(failed_attempts),
341
+ )
342
+
343
+ strategy = self._strategies[persona]
344
+
345
+ # Build the prompt
346
+ prompt_parts = [
347
+ f"## Persona: {persona.value.title()}",
348
+ f"_{strategy.system_prompt}_",
349
+ "",
350
+ "## Problem Context",
351
+ problem_context,
352
+ "",
353
+ "## Current Approach (Not Working)",
354
+ current_approach,
355
+ "",
356
+ ]
357
+
358
+ if failed_attempts:
359
+ prompt_parts.extend([
360
+ "## Previous Failed Attempts",
361
+ *[f"- {attempt}" for attempt in failed_attempts],
362
+ "",
363
+ ])
364
+
365
+ prompt_parts.extend([
366
+ "## Lateral Thinking Instructions",
367
+ *[f"{instr}" for instr in strategy.approach_instructions],
368
+ "",
369
+ "## Questions to Consider",
370
+ *[f"- {q}" for q in strategy.question_templates],
371
+ "",
372
+ "## Your Alternative Approach",
373
+ "Based on the above, propose a fundamentally different approach:",
374
+ ])
375
+
376
+ prompt = "\n".join(prompt_parts)
377
+
378
+ # Generate questions specific to this problem
379
+ questions = tuple(
380
+ q.format(
381
+ obstacle="the current blocker",
382
+ domain="this problem domain",
383
+ )
384
+ for q in strategy.question_templates
385
+ )
386
+
387
+ result = LateralThinkingResult(
388
+ persona=persona,
389
+ prompt=prompt,
390
+ approach_summary=f"{persona.value.title()}: {persona.description}",
391
+ questions=questions,
392
+ )
393
+
394
+ log.info(
395
+ "resilience.lateral.generated",
396
+ persona=persona.value,
397
+ prompt_length=len(prompt),
398
+ questions_count=len(questions),
399
+ )
400
+
401
+ return Result.ok(result)
402
+
403
+ def suggest_persona_for_pattern(
404
+ self,
405
+ pattern: StagnationPattern,
406
+ *,
407
+ exclude_personas: tuple[ThinkingPersona, ...] = (),
408
+ ) -> ThinkingPersona | None:
409
+ """Suggest the best persona for a given stagnation pattern.
410
+
411
+ Considers persona affinities and excludes already-tried personas.
412
+
413
+ Args:
414
+ pattern: The detected stagnation pattern.
415
+ exclude_personas: Personas to exclude from consideration.
416
+
417
+ Returns:
418
+ Best matching persona, or None if all excluded.
419
+ """
420
+ # Find personas with affinity for this pattern
421
+ candidates = [
422
+ persona
423
+ for persona in ThinkingPersona
424
+ if pattern in persona.affinity_patterns and persona not in exclude_personas
425
+ ]
426
+
427
+ if candidates:
428
+ # Return first (highest affinity since we defined them in priority order)
429
+ return candidates[0]
430
+
431
+ # Fall back to any non-excluded persona
432
+ remaining = [p for p in ThinkingPersona if p not in exclude_personas]
433
+ return remaining[0] if remaining else None
434
+
435
+ def get_all_personas(self) -> tuple[ThinkingPersona, ...]:
436
+ """Get all available thinking personas.
437
+
438
+ Returns:
439
+ Tuple of all ThinkingPersona values.
440
+ """
441
+ return tuple(ThinkingPersona)
442
+
443
+
444
+ # =============================================================================
445
+ # Event Classes
446
+ # =============================================================================
447
+
448
+
449
+ class LateralThinkingActivatedEvent(BaseEvent):
450
+ """Event emitted when lateral thinking is activated.
451
+
452
+ Indicates a persona has been selected to address stagnation.
453
+ """
454
+
455
+ def __init__(
456
+ self,
457
+ execution_id: str,
458
+ persona: ThinkingPersona,
459
+ stagnation_pattern: StagnationPattern | None,
460
+ *,
461
+ seed_id: str | None = None,
462
+ iteration: int = 0,
463
+ reason: str = "",
464
+ ) -> None:
465
+ """Create LateralThinkingActivatedEvent.
466
+
467
+ Args:
468
+ execution_id: Execution identifier.
469
+ persona: The selected thinking persona.
470
+ stagnation_pattern: Pattern that triggered activation (if any).
471
+ seed_id: Optional seed identifier.
472
+ iteration: Current iteration number.
473
+ reason: Human-readable reason for activation.
474
+ """
475
+ super().__init__(
476
+ type="resilience.lateral.activated",
477
+ aggregate_type="execution",
478
+ aggregate_id=execution_id,
479
+ data={
480
+ "persona": persona.value,
481
+ "stagnation_pattern": stagnation_pattern.value if stagnation_pattern else None,
482
+ "seed_id": seed_id,
483
+ "iteration": iteration,
484
+ "reason": reason,
485
+ },
486
+ )
487
+
488
+
489
+ class LateralThinkingSucceededEvent(BaseEvent):
490
+ """Event emitted when lateral thinking breaks through stagnation.
491
+
492
+ Indicates a persona successfully produced a working alternative.
493
+ """
494
+
495
+ def __init__(
496
+ self,
497
+ execution_id: str,
498
+ persona: ThinkingPersona,
499
+ *,
500
+ seed_id: str | None = None,
501
+ iteration: int = 0,
502
+ breakthrough_summary: str = "",
503
+ ) -> None:
504
+ """Create LateralThinkingSucceededEvent.
505
+
506
+ Args:
507
+ execution_id: Execution identifier.
508
+ persona: The persona that succeeded.
509
+ seed_id: Optional seed identifier.
510
+ iteration: Current iteration number.
511
+ breakthrough_summary: Brief description of the breakthrough.
512
+ """
513
+ super().__init__(
514
+ type="resilience.lateral.succeeded",
515
+ aggregate_type="execution",
516
+ aggregate_id=execution_id,
517
+ data={
518
+ "persona": persona.value,
519
+ "seed_id": seed_id,
520
+ "iteration": iteration,
521
+ "breakthrough_summary": breakthrough_summary[:500],
522
+ },
523
+ )
524
+
525
+
526
+ class LateralThinkingFailedEvent(BaseEvent):
527
+ """Event emitted when a lateral thinking attempt fails.
528
+
529
+ Indicates a persona did not produce a working alternative.
530
+ """
531
+
532
+ def __init__(
533
+ self,
534
+ execution_id: str,
535
+ persona: ThinkingPersona,
536
+ *,
537
+ seed_id: str | None = None,
538
+ iteration: int = 0,
539
+ failure_reason: str = "",
540
+ ) -> None:
541
+ """Create LateralThinkingFailedEvent.
542
+
543
+ Args:
544
+ execution_id: Execution identifier.
545
+ persona: The persona that failed.
546
+ seed_id: Optional seed identifier.
547
+ iteration: Current iteration number.
548
+ failure_reason: Reason the persona's approach failed.
549
+ """
550
+ super().__init__(
551
+ type="resilience.lateral.failed",
552
+ aggregate_type="execution",
553
+ aggregate_id=execution_id,
554
+ data={
555
+ "persona": persona.value,
556
+ "seed_id": seed_id,
557
+ "iteration": iteration,
558
+ "failure_reason": failure_reason[:500],
559
+ },
560
+ )
561
+
562
+
563
+ class AllPersonasExhaustedEvent(BaseEvent):
564
+ """Event emitted when all personas have been tried without success.
565
+
566
+ Indicates resilience has exhausted lateral thinking options.
567
+ """
568
+
569
+ def __init__(
570
+ self,
571
+ execution_id: str,
572
+ tried_personas: tuple[ThinkingPersona, ...],
573
+ *,
574
+ seed_id: str | None = None,
575
+ iteration: int = 0,
576
+ ) -> None:
577
+ """Create AllPersonasExhaustedEvent.
578
+
579
+ Args:
580
+ execution_id: Execution identifier.
581
+ tried_personas: All personas that were attempted.
582
+ seed_id: Optional seed identifier.
583
+ iteration: Current iteration number.
584
+ """
585
+ super().__init__(
586
+ type="resilience.lateral.exhausted",
587
+ aggregate_type="execution",
588
+ aggregate_id=execution_id,
589
+ data={
590
+ "tried_personas": [p.value for p in tried_personas],
591
+ "total_personas": len(ThinkingPersona),
592
+ "seed_id": seed_id,
593
+ "iteration": iteration,
594
+ },
595
+ )