Ctrl K
ComunidadGuías…

butler/gateway/locked_phase_registry.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Declarative locked-turn phase order (ENG-11)."""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Optional

if TYPE_CHECKING:
    from butler.gateway.locked_phases import LockedTurnState
    from butler.gateway.message_handler import ButlerMessageHandler


class PhaseKind(str, Enum):
    EARLY_EXIT = "early_exit"
    SETUP = "setup"
    VALIDATE_EXIT = "validate_exit"
    BODY = "body"


@dataclass(frozen=True)
class LockedPhaseEntry:
    name: str
    kind: PhaseKind
    fn: Any
    needs_handler: bool = True


def _pre_lock_entries() -> tuple[LockedPhaseEntry, ...]:
    from butler.gateway.locked_phases import (
        _phase_apply_correction_intent,
        _phase_apply_github_issues_intent,
        _phase_apply_normalizers_and_slash,
        _phase_apply_prompt_hooks,
        _phase_init_loop_role,
    )

    return (
        LockedPhaseEntry("normalizers_and_slash", PhaseKind.EARLY_EXIT, _phase_apply_normalizers_and_slash),
        LockedPhaseEntry("correction_intent", PhaseKind.EARLY_EXIT, _phase_apply_correction_intent),
        LockedPhaseEntry("github_issues_intent", PhaseKind.EARLY_EXIT, _phase_apply_github_issues_intent),
        LockedPhaseEntry("init_loop_role", PhaseKind.SETUP, _phase_init_loop_role),
        LockedPhaseEntry("prompt_hooks", PhaseKind.EARLY_EXIT, _phase_apply_prompt_hooks, needs_handler=False),
    )


def _in_context_entries() -> tuple[LockedPhaseEntry, ...]:
    from butler.gateway.locked_phases import (
        _phase_augment_prompt,
        _phase_execute_turn,
        _phase_finalize_turn,
        _phase_format_turn_response,
        _phase_hygiene_compress,
        _phase_prefetch_and_callbacks,
        _phase_resolve_turn_budget,
        _phase_validate_loop_messages,
    )

    return (
        LockedPhaseEntry("augment_prompt", PhaseKind.SETUP, _phase_augment_prompt),
        LockedPhaseEntry("validate_loop_messages", PhaseKind.VALIDATE_EXIT, _phase_validate_loop_messages, needs_handler=False),
        LockedPhaseEntry("resolve_turn_budget", PhaseKind.BODY, _phase_resolve_turn_budget, needs_handler=False),
        LockedPhaseEntry("hygiene_compress", PhaseKind.BODY, _phase_hygiene_compress),
        LockedPhaseEntry("prefetch_and_callbacks", PhaseKind.BODY, _phase_prefetch_and_callbacks),
        LockedPhaseEntry("execute_turn", PhaseKind.BODY, _phase_execute_turn, needs_handler=False),
        LockedPhaseEntry("finalize_turn", PhaseKind.BODY, _phase_finalize_turn),
        LockedPhaseEntry("format_turn_response", PhaseKind.BODY, _phase_format_turn_response),
    )


EARLY_EXIT_PHASE_NAMES: tuple[str, ...] = tuple(
    e.name for e in _pre_lock_entries() if e.kind == PhaseKind.EARLY_EXIT
)
IN_CONTEXT_PHASE_NAMES: tuple[str, ...] = tuple(e.name for e in _in_context_entries())


def run_pre_lock_phases(
    handler: "ButlerMessageHandler",
    state: "LockedTurnState",
) -> Optional[str]:
    """Phases before ``use_execution_context`` (may return early)."""
    for entry in _pre_lock_entries():
        if entry.kind == PhaseKind.EARLY_EXIT:
            response = _call_early_exit(handler, state, entry)
            if response is None:
                continue
            if entry.name == "github_issues_intent":
                from butler.gateway.gateway_transcript import record_gateway_tool_action

                record_gateway_tool_action(
                    state.session_key,
                    tool_name="mcp_github_lst_repo_issues",
                    args_preview=state.text.strip()[:400],
                )
            return response
        _call_setup(handler, state, entry)
    return None


def run_augment_phase(
    handler: "ButlerMessageHandler",
    state: "LockedTurnState",
) -> None:
    """First in-context step (before loop bind)."""
    from butler.gateway.locked_phases import _phase_augment_prompt

    _phase_augment_prompt(handler, state)


def run_in_context_phases(
    handler: "ButlerMessageHandler",
    state: "LockedTurnState",
    *,
    welcome_prefix: str = "",
) -> Optional[str]:
    """Phases inside ``use_execution_context`` (after loop bind)."""
    for entry in _in_context_entries():
        if entry.name == "augment_prompt":
            continue
        if entry.kind == PhaseKind.VALIDATE_EXIT:
            from typing import cast

            response = cast(Optional[str], entry.fn(state))
            if response is not None:
                return response
            continue
        if entry.kind == PhaseKind.BODY and entry.name == "format_turn_response":
            entry.fn(handler, state, welcome_prefix=welcome_prefix)
            continue
        _call_setup(handler, state, entry)
    return None


def _call_early_exit(
    handler: "ButlerMessageHandler",
    state: "LockedTurnState",
    entry: LockedPhaseEntry,
) -> Optional[str]:
    from typing import cast

    if entry.needs_handler:
        return cast(Optional[str], entry.fn(handler, state))
    return cast(Optional[str], entry.fn(state))


def _call_setup(
    handler: "ButlerMessageHandler",
    state: "LockedTurnState",
    entry: LockedPhaseEntry,
) -> None:
    if entry.needs_handler:
        entry.fn(handler, state)
    else:
        entry.fn(state)


__all__ = [
    "EARLY_EXIT_PHASE_NAMES",
    "IN_CONTEXT_PHASE_NAMES",
    "LockedPhaseEntry",
    "PhaseKind",
    "run_augment_phase",
    "run_in_context_phases",
    "run_pre_lock_phases",
]