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