"""System prompt assembly from a list of source files. The gateway holds no prompt text: an agent names its granules (paths from ``config.py``) and :func:`assemble` concatenates them in that order, so the result is byte-for-byte identical for every session of the same agent as long as the files are. Each granule's hash is logged at assembly so a drifted prompt can be traced to the file that changed. """ from __future__ import annotations import hashlib import logging from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Iterable __all__ = ["assemble"] _log = logging.getLogger("beaver_gateway.core.prompt") def assemble(sources: Iterable[str | Path]) -> str: parts: list[str] = [] for source in sources: path = Path(source) text = path.read_text(encoding="utf-8").strip() digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:12] _log.info("prompt granule %s sha=%s bytes=%d", path, digest, len(text)) if text: parts.append(text) return "\n\n".join(parts) + "\n"