Ctrl K
ComunidadGuías…

agent/prompt_refresh.py

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

Archivo original ↗Preguntar a KiBot
"""Adopt an operator's prompt refresh request on a continuing session's next turn.

A continuing session replays its stored system prompt verbatim so the provider prefix cache stays
warm, which also means SOUL.md, skills and project-context edits never reach it before ``/new``.
``hermes sessions refresh-prompts`` records a refresh epoch in the profile's state.db
(``hermes_state_prompt_refresh``). At the start of the session's next turn this module compares
that epoch with the one stamped when the session's prompt was built (``model_config``) and, when
the prompt is older, rebuilds it once through the normal builder: history, session id and the
tools[] pin are kept, ``on_session_start`` does not fire, and the new bytes plus the epoch are
persisted so every later turn restores them verbatim. One prefix-cache break per request.

No request recorded means the stored prompt is reused exactly as before: the only extra work is
one ``state_meta`` point read. Nothing watches files and nothing changes mid-turn.
"""

from __future__ import annotations

import logging
from typing import Any

logger = logging.getLogger(__name__)

# model_config key: the refresh epoch the session's stored prompt already satisfies.
SESSION_EPOCH_KEY = "prompt_refresh_epoch"


def requested_epoch(agent: Any) -> int:
    """Newest refresh epoch recorded for the agent's session; 0 without a request (or a store that
    keeps none, such as a managed worker's facade)."""
    reader = getattr(getattr(agent, "_session_db", None), "prompt_refresh_epoch", None)
    session_id = getattr(agent, "session_id", None)
    if reader is None or not session_id:
        return 0
    try:
        epoch = reader(session_id)
    except Exception:
        logger.warning("Prompt refresh epoch read failed (session=%s); reusing the stored prompt.",
                       session_id, exc_info=True)
        return 0
    return epoch if isinstance(epoch, int) else 0


def _applied_epoch(session_row: Any) -> int:
    from hermes_state_sessions import _parse_model_config

    value = _parse_model_config((session_row or {}).get("model_config")).get(SESSION_EPOCH_KEY)
    return value if isinstance(value, int) else 0


def stored_prompt_refresh_epoch(agent: Any, session_row: Any) -> int:
    """The epoch to rebuild the stored prompt at, or 0 when the stored prompt is current."""
    epoch = requested_epoch(agent)
    if epoch and _applied_epoch(session_row) < epoch:
        return epoch
    agent._prompt_refresh_epoch = epoch
    return 0


def stamp_prompt_epoch(agent: Any, epoch: int) -> None:
    """Record that the agent's freshly built prompt satisfies ``epoch`` (read BEFORE the build, so a
    request that lands mid-build still fires on the next turn)."""
    agent._prompt_refresh_epoch = epoch
    if epoch:
        _persist_epoch(agent, epoch)


def _persist_epoch(agent: Any, epoch: int) -> bool:
    db = getattr(agent, "_session_db", None)
    if not db:
        return True
    try:
        db.patch_session_model_config(agent.session_id, {SESSION_EPOCH_KEY: epoch})
    except Exception:
        logger.warning("Could not record the prompt refresh epoch (session=%s); the refresh re-fires "
                       "next turn.", agent.session_id, exc_info=True)
        return False
    return True


def rebuild_for_refresh(agent: Any, system_message: Any, conversation_history: Any, epoch: int) -> None:
    """Rebuild the session's prompt from current sources and persist it with ``epoch``."""
    from agent.prompt_builder import clear_skills_system_prompt_cache
    from agent.surface_switch import stage_surface_switch_note
    from hermes_cli.observability.shared_metrics_efficiency import record_cache_break

    logger.info("Operator requested a system prompt refresh (epoch %d) for session %s; rebuilding it "
                "from current SOUL.md, skills and context files (one-time prefix-cache break).",
                epoch, agent.session_id)
    # Same boundary compaction uses: memory reloads from disk, plugin sections re-render.
    agent._invalidate_system_prompt()
    agent._auto_load_skills_resolved = False  # pinned skills (skills.auto_load) re-read too
    # As in the Bot Chat capability refresh: build the index from disk, not a cached render.
    clear_skills_system_prompt_cache(clear_snapshot=True)
    agent._cached_system_prompt = agent._build_system_prompt(system_message)
    record_cache_break(agent, "system_prompt_rebuild")
    stage_surface_switch_note(agent, agent._cached_system_prompt, conversation_history)
    db = getattr(agent, "_session_db", None)
    if db:
        try:
            db.update_system_prompt(agent.session_id, agent._cached_system_prompt)
        except Exception:
            logger.warning("Could not persist the refreshed system prompt (session=%s); the refresh "
                           "re-fires next turn.", agent.session_id, exc_info=True)
            return
    if _persist_epoch(agent, epoch):
        agent._prompt_refresh_epoch = epoch


def refresh_warm_prompt(agent: Any, system_message: Any, conversation_history: Any) -> None:
    """An agent kept warm across turns (gateway agent cache, TUI/Desktop) adopts a refresh request
    recorded after its prompt was set. Agents whose prompt did not come from the restore/build path
    (seeded maintenance agents, forks) carry no epoch and are left alone."""
    seen = getattr(agent, "_prompt_refresh_epoch", None)
    if agent._cached_system_prompt is None or not isinstance(seen, int) or not conversation_history:
        return
    epoch = requested_epoch(agent)
    if epoch > seen:
        rebuild_for_refresh(agent, system_message, conversation_history, epoch)