Source code for ralph.prompts.debug_dump

"""Helpers for persisting rendered prompts for debugging."""

from __future__ import annotations

import json
from collections import OrderedDict
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING

from ralph.prompts._multimodal_sidecar_entry import MultimodalSidecarEntry

if TYPE_CHECKING:
    from ralph.workspace.protocol import Workspace


def _normalized_phase(phase: str) -> str:
    return phase.replace("/", "_").replace(" ", "_")


[docs] def prompt_dump_path(phase: str) -> str: """Return the workspace-relative path for a phase's debug prompt dump.""" return f".agent/tmp/{_normalized_phase(phase)}_prompt.md"
[docs] def worker_prompt_dump_path(worker_namespace: Path, phase: str) -> Path: """Return the worker-local prompt dump path for a phase.""" return worker_namespace / "tmp" / f"{_normalized_phase(phase)}_prompt.md"
[docs] def multimodal_sidecar_path(phase: str) -> str: """Return the workspace-relative path for a phase's multimodal handoff sidecar.""" return f".agent/tmp/{_normalized_phase(phase)}_multimodal_handoff.json"
[docs] def worker_multimodal_sidecar_path(worker_namespace: Path, phase: str) -> Path: """Return the worker-local multimodal handoff sidecar path for a phase.""" return worker_namespace / "tmp" / f"{_normalized_phase(phase)}_multimodal_handoff.json"
[docs] def media_session_path(phase: str) -> str: """Path for the persistent media session index written by the MCP server. This file accumulates artifact metadata for each media file loaded during a session via read_media. The runner reads it at the next prompt materialization to carry media context forward across sessions. """ return f".agent/tmp/{_normalized_phase(phase)}_media_session.json"
[docs] def media_registry_path() -> str: """Path for the centralized media artifact registry. Maps artifact_id to full v2 metadata for cross-session replay lookup. """ return ".agent/tmp/media_registry.json"
[docs] def media_cache_artifact_path(artifact_id: str) -> str: """Path for the durable byte cache of a media artifact. Bytes written here survive the session and enable cross-session replay. """ return f".agent/tmp/media/{artifact_id}"
_SIDECAR_SCHEMA_VERSION = "2" def _sidecar_entry_identity(entry: MultimodalSidecarEntry) -> str: if entry.identity_key: return entry.identity_key if entry.source_uri: return f"source-uri:{entry.modality}:{entry.source_uri}" if entry.source_path: return f"source-path:{entry.modality}:{entry.source_path}" return f"artifact-id:{entry.artifact_id or entry.uri}"
[docs] def write_multimodal_sidecar( workspace: Workspace, phase: str, entries: list[MultimodalSidecarEntry], *, worker_namespace: Path | None = None, ) -> None: """Persist the phase multimodal handoff sidecar for shared or worker-local prompts.""" path = ( str(worker_multimodal_sidecar_path(worker_namespace, phase)) if worker_namespace is not None else multimodal_sidecar_path(phase) ) payload = { "schema_version": _SIDECAR_SCHEMA_VERSION, "phase": phase, "artifacts": [entry.to_dict() for entry in entries], } workspace.write(path, json.dumps(payload, indent=2))
[docs] def clear_multimodal_sidecar( workspace: Workspace, phase: str, *, worker_namespace: Path | None = None, ) -> None: """Remove the multimodal handoff sidecar for a shared or worker-local prompt.""" path = ( str(worker_multimodal_sidecar_path(worker_namespace, phase)) if worker_namespace is not None else multimodal_sidecar_path(phase) ) with suppress(Exception): workspace.remove(path)
[docs] def collect_media_entries_for_phase( workspace: Workspace, phase: str, ) -> list[MultimodalSidecarEntry]: """Read media entries from the persistent session index for a phase.""" path = media_session_path(phase) try: raw = workspace.read(path) except Exception: return [] try: data: dict[str, object] = json.loads(raw) artifacts = data.get("artifacts") if not isinstance(artifacts, list): return [] entries: OrderedDict[str, MultimodalSidecarEntry] = OrderedDict() for item in artifacts: if not isinstance(item, dict): continue try: entry = MultimodalSidecarEntry( artifact_id=str(item.get("artifact_id", "")), uri=str(item.get("uri", "")), mime_type=str(item.get("mime_type", "")), title=str(item.get("title", "")), modality=str(item.get("modality", "")), delivery=str(item.get("delivery", "resource_reference_replay")), reason=str(item.get("reason", "")), source_path=str(item.get("source_path", "")), cache_path=str(item.get("cache_path", "")), source_uri=str(item.get("source_uri", "")), block_type=str(item.get("block_type", "")), failure_kind=str(item.get("failure_kind", "")), identity_key=str(item.get("identity_key", "")), ) except Exception: continue entries[_sidecar_entry_identity(entry)] = entry return list(entries.values()) except Exception: return []
[docs] def dump_rendered_prompt( workspace: Workspace, phase: str, prompt: str, *, worker_namespace: Path | None = None, ) -> str: """Write the rendered prompt to the debug dump path and return the path.""" path = ( worker_prompt_dump_path(worker_namespace, phase) if worker_namespace is not None else Path(prompt_dump_path(phase)) ) workspace.write(str(path), prompt) return str(path)