orchestrata_ultimate.harness.healing

Reference › orchestrata_ultimate › harness › healing

The self-healing loop (SPEC-102 Workstream F): a bounded repair attempt for a failed trial. SelfHealingLoop.heal(failure, context) is called by the runner when a trial fails and healing is enabled. With a CXP dispatcher it runs up to three hops, each a CXPPacket sent from harness to the failing volk and bounded by HOP_TIMEOUT_SECONDS (30): hop 1 is a diagnostic (an error packet built by _diagnostic_payload), hop 2 a repair command carrying the earlier hops, and hop 3 an escalation naming the reasoning model as fallback. After each hop _extract_strategy looks in the response for a retry or patch indicator; the first valid one ends the loop with a successful HealingResult and a repaired trial. A timeout or dispatch error is recorded in the hop's HopDiagnostic. When all hops are used, the diagnostics are saved under the diagnostics directory and the result reports failure.

Without a dispatcher, _heal_fallback runs a simplified path: it logs the diagnostic, records a simulated retry with a doubled timeout, and reports success after two hops without running anything again. max_hops comes from HarnessConfig.max_healing_hops. Types are in models.

At a glance

Source src/orchestrata_ultimate/harness/healing.py
Lines of code 482
Classes 1
Functions 4
Imports (internal) orchestrata_ultimate.agents.volk_registry, orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.packet, orchestrata_ultimate.cxp.endpoint_config, orchestrata_ultimate.harness.models, orchestrata_ultimate.kernel.dispatcher
Imported by orchestrata_ultimate.harness.runner
Tests that import it tests/harness/test_cxp_live.py, tests/harness/test_healing.py

Class diagram

Class diagram orchestrata_ultimate.harness.healing

Dependency diagram

Dependency diagram orchestrata_ultimate.harness.healing

API

Constants

Name Type Value
HOP_TIMEOUT_SECONDS int 30

Classes

SelfHealingLoop

Decorators: traces(spec='102', story='US-003', reqs=['REQ-102-034', 'REQ-102-037', 'REQ-102-046', 'REQ-102-049']) · Line 106

Autonomous repair loop for failed scenario trials.

Method Signature Summary
__init__ __init__(dispatcher: MessageDispatcher | None, max_hops: int = 3, environ: Mapping[str, str] | None = None) -> None
heal async heal(failure: TrialResult, context: HarnessContext) -> HealingResult Attempt to heal failure using up to max_hops repair hops.
_run_hop async _run_hop(hop_number: int, action: str, failure: TrialResult, context: HarnessContext, diagnostics: list[HopDiagnostic]) -> HopDiagnostic Build and dispatch a single healing hop packet.
_has_valid_strategy _has_valid_strategy(hop: HopDiagnostic) -> bool Return True when the hop produced a usable repair strategy.
_repaired_trial _repaired_trial(failure: TrialResult, hops: int) -> TrialResult Return a new :class:TrialResult simulating a successful repair.
_heal_fallback async _heal_fallback(failure: TrialResult, context: HarnessContext) -> HealingResult Simplified internal retry when the dispatcher is unavailable.
_save_diagnostics async _save_diagnostics(failure: TrialResult, context: HarnessContext, diagnostics: list[HopDiagnostic]) -> None Persist the full diagnostic trace to a JSON file.
_resolve_safe_path _resolve_safe_path(preferred_dir: Path, filename: str, worktree: Path) -> Path Return a write path under worktree.
_assert_under_worktree _assert_under_worktree(path: Path, worktree: Path) -> None Raise PermissionError if path escapes worktree.

Functions

Function Signature Summary
_make_recipient traces _make_recipient(volk_name: str, environ: Mapping[str, str] | None = None) -> VolkDefinition Build a minimal :class:VolkDefinition for CXP dispatch.
_extract_strategy traces _extract_strategy(response: dict[str, Any] | None) -> str | None Look for retry or patch indicators in a dispatch response.
_diagnostic_payload traces _diagnostic_payload(failure: TrialResult, context: HarnessContext) -> dict[str, Any] Build the hop-1 diagnostic payload.
_hop_to_dict traces _hop_to_dict(hop: HopDiagnostic) -> dict[str, Any] Serialize a :class:HopDiagnostic to a plain dictionary.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/harness/healing.py

"""Self-healing loop for the Volk Test Harness.

Implements a 3-hop autonomous repair mechanism that dispatches diagnostic
CXP packets when a scenario trial fails.  If the CXP kernel is unavailable,
a simplified internal fallback retry is used.

Spec: 102 — Workstream F
"""

from __future__ import annotations

import asyncio
import json
import logging
from collections.abc import Mapping
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from orchestrata_ultimate.agents.volk_registry import VolkDefinition, VolkProtocol
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.packet import CXPPacket, PacketType
from orchestrata_ultimate.cxp.endpoint_config import cxp_grpc_endpoint
from orchestrata_ultimate.harness.models import (
    HarnessContext,
    HealingResult,
    HopDiagnostic,
    TrialResult,
)
from orchestrata_ultimate.kernel.dispatcher import MessageDispatcher

_logger = logging.getLogger(__name__)

HOP_TIMEOUT_SECONDS: int = 30


@traces(spec="102", story="US-003", reqs=["REQ-102-097"])
def _make_recipient(
    volk_name: str, environ: Mapping[str, str] | None = None
) -> VolkDefinition:
    """Build a minimal :class:`VolkDefinition` for CXP dispatch.

    Args:
        volk_name: Alias/name/role for the recipient definition.
        environ: Optional explicit environment mapping (for testing); when
            omitted, ``cxp_grpc_endpoint`` reads the real process environment.
    """
    return VolkDefinition(
        alias=volk_name,
        name=volk_name,
        role="repair",
        capabilities=[],
        protocols=[VolkProtocol.CXP],
        endpoint=cxp_grpc_endpoint(environ),
    )


@traces(spec="102", story="US-003", reqs=["REQ-102-042"])
def _extract_strategy(response: dict[str, Any] | None) -> str | None:
    """Look for *retry* or *patch* indicators in a dispatch response."""
    if not response:
        return None

    payload = response.get("payload", {})
    if isinstance(payload, dict):
        strategy = payload.get("strategy") or payload.get("action")
        if isinstance(strategy, str) and strategy in ("retry", "patch"):
            return strategy

    error_msg = (response.get("error_message") or "").lower()
    if "retry" in error_msg:
        return "retry"
    if "patch" in error_msg:
        return "patch"

    return None


@traces(spec="102", story="US-003", reqs=["REQ-102-037"])
def _diagnostic_payload(
    failure: TrialResult, context: HarnessContext
) -> dict[str, Any]:
    """Build the hop-1 diagnostic payload."""
    failed_evaluators = [er for er in failure.evaluator_results if not er.passed]
    return {
        "evaluator_results": [
            {
                "name": er.name,
                "passed": er.passed,
                "score": er.score,
                "details": er.details,
            }
            for er in failed_evaluators
        ],
        "error": failure.error,
        "snapshot_diff": failure.snapshot_diff,
        "scenario_id": context.scenario_id,
    }


@traces(
    spec="102",
    story="US-003",
    reqs=["REQ-102-034", "REQ-102-037", "REQ-102-046", "REQ-102-049"],
)
class SelfHealingLoop:
    """Autonomous repair loop for failed scenario trials.

    Uses the CXP dispatcher to send diagnostic packets and collect repair
    strategies.  Falls back to a simple internal retry when the dispatcher
    is not available.
    """

    @traces(spec="102", story="US-003", reqs=["REQ-102-035"])
    def __init__(
        self,
        dispatcher: MessageDispatcher | None,
        max_hops: int = 3,
        environ: Mapping[str, str] | None = None,
    ) -> None:
        self.dispatcher = dispatcher
        self.max_hops = max_hops
        self.environ = environ

    @traces(spec="102", story="US-003", reqs=["REQ-102-047"])
    async def heal(
        self, failure: TrialResult, context: HarnessContext
    ) -> HealingResult:
        """Attempt to heal *failure* using up to ``max_hops`` repair hops.

        Args:
            failure: The failed trial result to diagnose and repair.
            context: Runtime context (scenario, volk, worktree, config).

        Returns:
            A :class:`HealingResult` indicating success/failure and the
            number of hops consumed.
        """
        if self.dispatcher is None:
            return await self._heal_fallback(failure, context)

        diagnostics: list[HopDiagnostic] = []

        # Hop 1 — Diagnostic
        if len(diagnostics) < self.max_hops:
            hop1 = await self._run_hop(
                hop_number=1,
                action="diagnostic",
                failure=failure,
                context=context,
                diagnostics=diagnostics,
            )
            diagnostics.append(hop1)
            if self._has_valid_strategy(hop1):
                return HealingResult(
                    success=True,
                    hops_used=1,
                    diagnostics=diagnostics,
                    final_trial_result=self._repaired_trial(failure, hops=1),
                )

        # Hop 2 — Repair
        if len(diagnostics) < self.max_hops:
            hop2 = await self._run_hop(
                hop_number=2,
                action="repair",
                failure=failure,
                context=context,
                diagnostics=diagnostics,
            )
            diagnostics.append(hop2)
            if self._has_valid_strategy(hop2):
                return HealingResult(
                    success=True,
                    hops_used=2,
                    diagnostics=diagnostics,
                    final_trial_result=self._repaired_trial(failure, hops=2),
                )

        # Hop 3 — Escalation
        if len(diagnostics) < self.max_hops:
            hop3 = await self._run_hop(
                hop_number=3,
                action="escalation",
                failure=failure,
                context=context,
                diagnostics=diagnostics,
            )
            diagnostics.append(hop3)
            if self._has_valid_strategy(hop3):
                return HealingResult(
                    success=True,
                    hops_used=3,
                    diagnostics=diagnostics,
                    final_trial_result=self._repaired_trial(failure, hops=3),
                )

        # Exhausted — save trace and return failure
        await self._save_diagnostics(failure, context, diagnostics)
        return HealingResult(
            success=False,
            hops_used=self.max_hops,
            diagnostics=diagnostics,
        )

    # ------------------------------------------------------------------
    # Per-hop execution
    # ------------------------------------------------------------------

    @traces(
        spec="102",
        story="US-003",
        reqs=[
            "REQ-102-038",
            "REQ-102-040",
            "REQ-102-041",
            "REQ-102-043",
            "REQ-102-044",
            "REQ-102-045",
        ],
    )
    async def _run_hop(
        self,
        hop_number: int,
        action: str,
        failure: TrialResult,
        context: HarnessContext,
        diagnostics: list[HopDiagnostic],
    ) -> HopDiagnostic:
        """Build and dispatch a single healing hop packet."""
        to_agent = context.volk_name if context.volk_name else "repair"
        subject = f"healing:{context.scenario_id}:hop{hop_number}"

        if hop_number == 1:
            payload = _diagnostic_payload(failure, context)
            packet_type = PacketType.ERROR
        elif hop_number == 2:
            payload = {
                "action": "apply_repair",
                "previous_hops": [_hop_to_dict(h) for h in diagnostics],
                "scenario_id": context.scenario_id,
            }
            packet_type = PacketType.COMMAND
        else:
            payload = {
                "action": "escalate",
                "diagnostic_history": [_hop_to_dict(h) for h in diagnostics],
                "fallback_llm": context.config.reasoning_llm,
                "scenario_id": context.scenario_id,
            }
            packet_type = PacketType.ERROR

        packet = CXPPacket(
            from_agent="harness",
            to_agent=to_agent,
            subject=subject,
            payload=payload,
            packet_type=packet_type,
            priority=1,
        )

        recipient = _make_recipient(to_agent, environ=self.environ)

        dispatcher = self.dispatcher
        assert dispatcher is not None, "dispatcher checked in heal()"
        try:
            dispatch_result = await asyncio.wait_for(
                dispatcher.dispatch(packet, recipient),
                timeout=HOP_TIMEOUT_SECONDS,
            )
            response_dict = dispatch_result.to_dict()
            # Merge the outgoing packet payload so _extract_strategy can read
            # the strategy that the receiving volk is expected to echo back.
            response_dict["payload"] = packet.payload
            return HopDiagnostic(
                hop_number=hop_number,
                action=action,
                packet_sent=packet.to_dict(),
                response=response_dict,
                error=(
                    None if dispatch_result.success else dispatch_result.error_message
                ),
            )
        except TimeoutError:
            return HopDiagnostic(
                hop_number=hop_number,
                action=action,
                packet_sent=packet.to_dict(),
                response=None,
                error=f"Hop {hop_number} timeout after {HOP_TIMEOUT_SECONDS}s",
            )
        except Exception as exc:  # noqa: BLE001
            return HopDiagnostic(
                hop_number=hop_number,
                action=action,
                packet_sent=packet.to_dict(),
                response=None,
                error=f"{type(exc).__name__}: {exc}",
            )

    # ------------------------------------------------------------------
    # Strategy evaluation
    # ------------------------------------------------------------------

    @traces(spec="102", story="US-003", reqs=["REQ-102-042"])
    def _has_valid_strategy(self, hop: HopDiagnostic) -> bool:
        """Return *True* when the hop produced a usable repair strategy."""
        if hop.error:
            return False
        return _extract_strategy(hop.response) is not None

    @traces(spec="102", story="US-003", reqs=["REQ-102-046"])
    def _repaired_trial(self, failure: TrialResult, hops: int) -> TrialResult:
        """Return a new :class:`TrialResult` simulating a successful repair."""
        return TrialResult(
            trial_number=failure.trial_number,
            passed=True,
            evaluator_results=failure.evaluator_results,
            llm_judge_score=failure.llm_judge_score,
            snapshot_diff=failure.snapshot_diff,
            healing_hops_used=hops,
            execution_time_ms=failure.execution_time_ms,
            error=None,
        )

    # ------------------------------------------------------------------
    # Fallback (no CXP dispatcher)
    # ------------------------------------------------------------------

    @traces(spec="102", story="US-003", reqs=["REQ-102-049"])
    async def _heal_fallback(
        self, failure: TrialResult, context: HarnessContext
    ) -> HealingResult:
        """Simplified internal retry when the dispatcher is unavailable.

        Hop 1: Log diagnostics.
        Hop 2: Retry with doubled timeout (simulated).
        Hop 3: Retry with reasoning LLM (simulated).
        """
        diagnostics: list[HopDiagnostic] = []

        # Hop 1 — log
        _logger.info(
            "Healing fallback hop 1 (diagnostic) for %s trial %d: error=%s",
            context.scenario_id,
            context.trial_number,
            failure.error,
        )
        diagnostics.append(
            HopDiagnostic(
                hop_number=1,
                action="fallback_diagnostic",
                packet_sent={"log": "Diagnostic details logged"},
                response=None,
                error=None,
            )
        )

        # Hop 2 — doubled timeout
        _logger.info(
            "Healing fallback hop 2 (doubled timeout) for %s",
            context.scenario_id,
        )
        diagnostics.append(
            HopDiagnostic(
                hop_number=2,
                action="fallback_retry_doubled_timeout",
                packet_sent={"timeout_multiplier": 2},
                response={"simulated": "retry with 2x timeout"},
                error=None,
            )
        )
        # In fallback mode we optimistically assume the retry succeeds.
        return HealingResult(
            success=True,
            hops_used=2,
            diagnostics=diagnostics,
            final_trial_result=self._repaired_trial(failure, hops=2),
        )

    # ------------------------------------------------------------------
    # Diagnostics persistence
    # ------------------------------------------------------------------

    @traces(spec="102", story="US-003", reqs=["REQ-102-047"])
    async def _save_diagnostics(
        self,
        failure: TrialResult,
        context: HarnessContext,
        diagnostics: list[HopDiagnostic],
    ) -> None:
        """Persist the full diagnostic trace to a JSON file.

        Writes are restricted to ``context.worktree_path`` for safety.
        """
        timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
        filename = f"{context.scenario_id}_{timestamp}.json"

        diagnostics_dir = context.config.diagnostics_dir
        output_path = self._resolve_safe_path(
            preferred_dir=diagnostics_dir,
            filename=filename,
            worktree=context.worktree_path,
        )

        output_path.parent.mkdir(parents=True, exist_ok=True)
        self._assert_under_worktree(output_path, context.worktree_path)

        data: dict[str, Any] = {
            "scenario_id": context.scenario_id,
            "volk_name": context.volk_name,
            "trial_number": context.trial_number,
            "original_failure": {
                "trial_number": failure.trial_number,
                "passed": failure.passed,
                "error": failure.error,
                "snapshot_diff": failure.snapshot_diff,
                "evaluator_results": [
                    {
                        "name": er.name,
                        "passed": er.passed,
                        "score": er.score,
                        "details": er.details,
                    }
                    for er in failure.evaluator_results
                ],
            },
            "hops": [_hop_to_dict(h) for h in diagnostics],
            "timestamp": datetime.now(UTC).isoformat(),
        }

        output_path.write_text(
            json.dumps(data, indent=2, default=str),
            encoding="utf-8",
        )
        _logger.info("Saved diagnostic trace to %s", output_path)

    # ------------------------------------------------------------------
    # Path safety
    # ------------------------------------------------------------------

    @traces(spec="102", story="US-003", reqs=["REQ-102-048"])
    def _resolve_safe_path(
        self, preferred_dir: Path, filename: str, worktree: Path
    ) -> Path:
        """Return a write path under *worktree*.

        If *preferred_dir* is not inside *worktree*, fall back to
        ``worktree / "diagnostics" / filename``.
        """
        try:
            preferred_dir.resolve().relative_to(worktree.resolve())
            return preferred_dir / filename
        except ValueError:
            _logger.warning(
                "Diagnostics dir %s is outside worktree %s; using worktree fallback",
                preferred_dir,
                worktree,
            )
            return worktree / "diagnostics" / filename

    @traces(spec="102", story="US-003", reqs=["REQ-102-048", "REQ-102-123"])
    def _assert_under_worktree(self, path: Path, worktree: Path) -> None:
        """Raise *PermissionError* if *path* escapes *worktree*."""
        try:
            path.resolve().relative_to(worktree.resolve())
        except ValueError as exc:
            raise PermissionError(
                f"Cannot write diagnostics outside worktree: {path}"
            ) from exc


@traces(spec="102", story="US-003", reqs=["REQ-102-047"])
def _hop_to_dict(hop: HopDiagnostic) -> dict[str, Any]:
    """Serialize a :class:`HopDiagnostic` to a plain dictionary."""
    return {
        "hop_number": hop.hop_number,
        "action": hop.action,
        "packet_sent": hop.packet_sent,
        "response": hop.response,
        "error": hop.error,
    }