Key Patterns

June 5, 2026 ยท View on GitHub

Core patterns used throughout the Ouroboros codebase. Understanding these will help you write idiomatic contributions.

1. Result Type for Error Handling

Ouroboros uses Result[T, E] instead of exceptions for expected failures. Exceptions are reserved for programming errors (bugs).

Location: src/ouroboros/core/types.py

from ouroboros.core.types import Result
from ouroboros.core.errors import ValidationError

# Returning success
def validate_score(score: float) -> Result[float, ValidationError]:
    if 0.0 <= score <= 1.0:
        return Result.ok(score)
    return Result.err(ValidationError(f"Score {score} out of range"))

# Consuming results
result = validate_score(0.85)
if result.is_ok:
    process(result.value)
else:
    log_error(result.error.message)

# Chaining
result = (
    validate_score(0.85)
    .map(lambda s: s * 100)           # Transform Ok value
    .map_err(lambda e: str(e))        # Transform Err value
)

# FlatMap / bind
result = validate_score(0.85).and_then(lambda s: further_validation(s))

Rules:

  • Functions that can fail due to external factors (LLM calls, file I/O, validation) return Result
  • Functions that should never fail (pure transforms, property access) raise exceptions on bugs
  • Always check is_ok or is_err before accessing .value or .error

2. Frozen Dataclasses for Immutability

All data models are frozen (immutable after construction) for thread safety and predictability.

from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class CheckResult:
    check_type: CheckType
    passed: bool
    message: str
    details: dict[str, Any] = field(default_factory=dict)

Pydantic models use frozen=True for the same effect:

from pydantic import BaseModel, Field

class Seed(BaseModel, frozen=True):
    goal: str = Field(..., min_length=1)
    constraints: tuple[str, ...] = Field(default_factory=tuple)

Rules:

  • Use frozen=True, slots=True on dataclasses
  • Use frozen=True on Pydantic BaseModel
  • Use tuple[...] instead of list[...] for frozen collections
  • If you need to "modify", create a new instance with changed values

3. Event Sourcing

All state changes are recorded as immutable events. The EventStore is append-only.

Location: src/ouroboros/events/, src/ouroboros/persistence/event_store.py

from ouroboros.events.base import BaseEvent

# Events are created by factory functions
event = create_stage1_completed_event(
    execution_id="exec_123",
    passed=True,
    checks=[...],
    coverage_score=0.85,
)

# Events are appended to the store
await event_store.append(event)

# Events are replayed for state reconstruction
events = await event_store.replay(aggregate_id="exec_123")

Rules:

  • Events are immutable (BaseEvent is frozen)
  • Never modify events after creation
  • Use factory functions to create events (they set timestamps, IDs, etc.)
  • The TUI polls the EventStore to update its display

4. Protocol Classes for Pluggable Strategies

Use Protocol (not abstract base classes) for defining interfaces that multiple implementations satisfy.

Location: src/ouroboros/orchestrator/execution_strategy.py

from typing import Protocol, runtime_checkable

@runtime_checkable
class ExecutionStrategy(Protocol):
    def get_tools(self) -> list[str]: ...
    def get_system_prompt_fragment(self) -> str: ...
    def get_task_prompt_suffix(self) -> str: ...
    def get_activity_map(self) -> dict[str, ActivityType]: ...

class CodeStrategy:
    """Satisfies ExecutionStrategy implicitly -- no inheritance needed."""
    def get_tools(self) -> list[str]:
        return ["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
    # ... other methods

Registration:

# Built-in strategies
_STRATEGY_REGISTRY: dict[str, ExecutionStrategy] = {
    "code": CodeStrategy(),
    "research": ResearchStrategy(),
    "analysis": AnalysisStrategy(),
}

# Custom strategies
register_strategy("custom", MyCustomStrategy())

Rules:

  • Use Protocol for interfaces, not ABC
  • Add @runtime_checkable if you need isinstance() checks
  • Implementations satisfy the protocol by duck typing (no class Foo(ExecutionStrategy))
  • Register in the module-level registry

5. Three-Stage Evaluation Pipeline

Every artifact goes through up to three evaluation stages:

Artifact --> Stage 1: Mechanical (\$0)     -- lint, build, test, static, coverage
         --> Stage 2: Semantic ($$)       -- LLM evaluates AC compliance + drift
         --> Stage 3: Consensus ($$$$)    -- multi-model vote (only if triggered)

Key classes:

  • EvaluationPipeline orchestrates the stages
  • MechanicalVerifier runs shell commands
  • SemanticEvaluator calls LLM for semantic analysis
  • ConsensusEvaluator / DeliberativeConsensus runs multi-model voting
  • ConsensusTrigger decides whether Stage 3 is needed (6 trigger conditions)

Stage 3 triggers:

  1. Seed modification
  2. Ontology evolution
  3. Goal reinterpretation
  4. Seed drift > 0.3
  5. Stage 2 uncertainty > 0.3
  6. Lateral thinking adoption

6. TUI State Management

The TUI uses Textual's reactive system with a strict single source of truth.

Location: src/ouroboros/tui/app.py, src/ouroboros/tui/events.py

# TUIState is the SSOT -- owned by app.py
@dataclass
class TUIState:
    current_phase: str = ""
    ac_tree: dict[str, Any] = field(default_factory=dict)
    drift_score: float = 0.0
    total_cost: float = 0.0
    # ...

# Events from EventStore are converted to Textual messages
message = create_message_from_event(event)
app.post_message(message)

# Screens handle messages to update their display
class DashboardScreenV3(Screen):
    def on_ac_updated(self, message: ACUpdated) -> None:
        self._update_tree_node(message.ac_id, message.status)

Rules:

  • app.py owns _state -- screens read from it
  • Do not mutate Textual reactive dicts in-place then reassign the same reference
  • Use is not None (not truthiness) for index checks (falsy-0 pitfall)
  • DashboardScreenV3 uses _tree (SelectableACTree), not _ac_tree (legacy widget)

7. Seed Immutability

The Seed is frozen after creation. Direction fields (goal, constraints, acceptance_criteria) are immutable.

seed = Seed(
    goal="Build a CLI tool",
    constraints=("Python >= 3.12",),
    acceptance_criteria=("Create main.py",),
    ontology_schema=OntologySchema(name="CLI", description="CLI tool"),
    metadata=SeedMetadata(ambiguity_score=0.15),
)

# This raises an error:
seed.goal = "Different goal"  # ValidationError: frozen

Rules:

  • Never attempt to modify a Seed
  • The ontology can evolve with consensus, but direction fields cannot
  • Use seed.to_dict() / Seed.from_dict() for serialization

8. Capability Graph for Skill Execution

Skills declare abstract runtime needs; runtime adapters translate those needs through the capability graph.

Location: src/ouroboros/backends/capabilities.py

SkillExecutionCapability(
    name="run_lateral_review",
    guidance=(
        "When an interview response marks `lateral_review_required=true`, "
        "call `ouroboros_lateral_think` with the supplied tool args before "
        "routing the next interview turn."
    ),
)

Rules:

  • Keep SKILL.md capability names runtime-neutral
  • Put runtime-specific tool names and prompt wording inside SkillExecutionCapability.guidance
  • Regenerate or update setup-owned instruction artifacts when rendered guide output changes
  • Follow docs/runtime-guides/skill-capability-guides.md before merging capability changes

Summary Table

PatternWhen to UseKey File
Result[T, E]Any function that can failcore/types.py
Frozen dataclassAll data modelsThroughout
Event sourcingState changespersistence/event_store.py
Protocol classesPluggable interfacesorchestrator/execution_strategy.py
Three-stage evalArtifact verificationevaluation/pipeline.py
TUI SSOTUI state managementtui/app.py, tui/events.py
Seed immutabilityWorkflow specificationcore/seed.py
Capability graphRuntime-specific skill executionbackends/capabilities.py