Skip to content

symfonic.memory.render

render

Shared MEMORY_CONTEXT render helpers.

v7.7 (Jarvio procedural-render-bypass follow-up). The engine's hydrate render loop used to enumerate a fixed metadata key set inline at engine.py:5237-5345 (description, name, role, properties). v7.6.4 patched the procedural case via an inline dispatch; v7.7 finishes the architecture by moving the legacy render shape into this module and adding a render_for_prompt protocol on BaseMemoryStore so each layer owns its render contract.

Layers that don't need a layer-specific shape delegate to :func:render_legacy -- this preserves the pre-v7.7 [layer] content (k=v) output byte-identically so prompt-cache parity holds for adopters who haven't opted into any new metadata surface.

The credential-scrubber callable threads in as the second positional argument so v6.1.9 F2 defense-in-depth (scrub credential-shaped keys from properties dicts BEFORE formatting) still runs through every render path. None is permitted -- it disables scrubbing for the common case where no credential pattern is configured.

render_legacy

render_legacy(
    entry: Any,
    scrubber: Callable[[dict[str, Any]], dict[str, Any]]
    | None = None,
) -> str

Render an entry in the pre-v7.7 [layer] content (k=v) shape.

Body ported byte-identically from engine.py:5275-5345 so any layer delegating here emits the same string the engine used to emit before the polymorphic-dispatch refactor. This is the prompt-cache-parity guarantee for adopters who haven't migrated to a layer-specific render shape.

Parameters:

Name Type Description Default
entry Any

A retrieved memory entry. The helper reads .content, .layer, and .metadata defensively (any missing attribute degrades to a sentinel) so it tolerates the duck-typed stubs tests use as well as full MemoryEntry pydantic models.

required
scrubber Callable[[dict[str, Any]], dict[str, Any]] | None

Optional credential-scrubber callable. When the entry's metadata carries a properties dict, every value flows through this callable before formatting -- v6.1.9 F2 defense-in-depth. None skips scrubbing.

None

Returns:

Type Description
str

The fully-rendered line including the leading [<layer>]

str

tag. Multi-line content stays one line in the rendered

str

output (the legacy shape never inserted line breaks).

Source code in src/symfonic/memory/render.py
def render_legacy(
    entry: Any,
    scrubber: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
) -> str:
    """Render an entry in the pre-v7.7 ``[layer] content (k=v)`` shape.

    Body ported byte-identically from ``engine.py:5275-5345`` so any
    layer delegating here emits the same string the engine used to
    emit before the polymorphic-dispatch refactor.  This is the
    prompt-cache-parity guarantee for adopters who haven't migrated
    to a layer-specific render shape.

    Args:
        entry: A retrieved memory entry.  The helper reads
            ``.content``, ``.layer``, and ``.metadata`` defensively
            (any missing attribute degrades to a sentinel) so it
            tolerates the duck-typed stubs tests use as well as
            full ``MemoryEntry`` pydantic models.
        scrubber: Optional credential-scrubber callable.  When the
            entry's metadata carries a ``properties`` dict, every
            value flows through this callable before formatting --
            v6.1.9 F2 defense-in-depth.  ``None`` skips scrubbing.

    Returns:
        The fully-rendered line including the leading ``[<layer>]``
        tag.  Multi-line content stays one line in the rendered
        output (the legacy shape never inserted line breaks).
    """
    content = getattr(entry, "content", str(entry))
    layer_name = getattr(entry, "layer", "unknown")
    metadata = getattr(entry, "metadata", {}) or {}

    # Defense-in-depth (Defect A): strip a leading machine-label
    # token (``AGENT_IDENTITY:`` / ``SOUL:``) so legacy nodes that
    # baked the label into ``content`` cannot leak it into the
    # MEMORY_CONTEXT block the model echoes back.
    content = strip_memory_label_prefix(str(content))

    line = f"[{layer_name}] {content}"
    if not metadata:
        return line

    extra: list[str] = []
    for key in _LEGACY_FIXED_KEYS:
        val = metadata.get(key)
        if not val or not isinstance(val, (str, dict)):
            continue
        if isinstance(val, dict):
            # v6.1.9 F2: scrub credential-shaped keys from the dict
            # BEFORE formatting, so pre-existing secrets written
            # under older versions cannot be echoed back into the
            # wire payload.
            safe_val = scrubber(val) if scrubber is not None else val
            for k, v in safe_val.items():
                if isinstance(v, str):
                    extra.append(f"{k}={v}")
        else:
            extra.append(f"{key}={val}")

    if extra:
        line += f" ({', '.join(extra[:_LEGACY_EXTRA_CAP])})"
    return line

strip_memory_label_prefix

strip_memory_label_prefix(content: str) -> str

Strip a leading AGENT_IDENTITY: / SOUL: token from content.

Public successor to the v6.x _strip_memory_label_prefix helper that lived in engine.py:389-402. Moved here in v7.7 so per- layer renderers can apply the same Defect A defense-in-depth without depending on the engine module.

Returns the content unchanged when no prefix is found OR when the input is empty.

Source code in src/symfonic/memory/render.py
def strip_memory_label_prefix(content: str) -> str:
    """Strip a leading ``AGENT_IDENTITY:`` / ``SOUL:`` token from content.

    Public successor to the v6.x ``_strip_memory_label_prefix`` helper
    that lived in ``engine.py:389-402``.  Moved here in v7.7 so per-
    layer renderers can apply the same Defect A defense-in-depth
    without depending on the engine module.

    Returns the content unchanged when no prefix is found OR when the
    input is empty.
    """
    if not content:
        return content
    stripped = content.lstrip()
    for prefix in _MEMORY_LABEL_PREFIXES:
        if stripped.upper().startswith(prefix):
            return stripped[len(prefix):].lstrip()
    return content