Skip to content

symfonic.core.plugins.contribution

contribution

PluginContribution dataclass + position vocabulary.

Item 14 (7.4) — richer plugin-contribution model that lets plugins emit content at multiple cache positions (cached prefix vs. volatile suffix) without invalidating the cached prefix on every warm turn.

See BaseDomainPlugin.inject_contributions (documented in symfonic.core.plugins.base) for the full contract. Design note: .claude/docs/2026-05-26-plugin-api-redesign-design.md.

PluginContribution dataclass

PluginContribution(
    content: str,
    position: Position = "cached",
    order: int = 100,
    header: str | None = None,
    origin: str | None = None,
    cache_ttl: CacheTtl | None = None,
)

A single section emitted by a plugin into a cache position.

Attributes:

Name Type Description
content str

Rendered text. Empty string skips the contribution silently (no header, no separator).

position Position

"cached" (L1 / cached prefix, the default), "volatile" (L2 / per-turn uncached suffix), or "kernel" (RESERVED in 7.4 — accepted-but-no-op; the dispatcher remaps to "cached" with a one-time WARNING).

order int

Sort key within a position. LOWER values render first. Default 100. Negative values are allowed; use them when a plugin needs to render before the default-ordered plugins.

header str | None

None (default) — engine auto-injects ### {plugin.name} on the first contribution per (plugin, position) group. "" — no header at all (plugin owns its scoping). Any non-empty string — used verbatim as the header on this contribution.

origin str | None

Engine-populated diagnostic field carrying the plugin name. Plugins MUST NOT set this; the dispatcher fills it in during dispatch so the auto-header logic and observability tooling can attribute contributions to plugins.

Raises:

Type Description
ValueError

If position is not one of "cached" / "volatile" / "kernel".

TypeError

If content is not a str.

with_origin

with_origin(origin: str) -> PluginContribution

Return a copy carrying the supplied origin (plugin name).

Source code in src/symfonic/core/plugins/contribution.py
def with_origin(self, origin: str) -> PluginContribution:
    """Return a copy carrying the supplied ``origin`` (plugin name)."""
    return replace(self, origin=origin)

render_position_group

render_position_group(
    contributions: list[PluginContribution],
    position: Position,
) -> str | None

Render contributions at a single position into one joined string.

Applies the header rule (§9 of the design): the FIRST contribution per (origin, position) gets an auto-header of ### {origin} when its header is None; subsequent contributions from the SAME origin at the SAME position emit raw content (no duplicate header). header="" always means "no header". A non-empty explicit header is emitted verbatim before the content.

Empty-string content is skipped silently.

Parameters:

Name Type Description Default
contributions list[PluginContribution]

All contributions for the current turn (any position). The function filters to the requested position internally.

required
position Position

Which position group to render.

required

Returns:

Type Description
str | None

Joined string for the position, or None when no contribution

str | None

at this position produced renderable text. Returning None

str | None

(instead of "") lets the engine drop the section cleanly

str | None

from its layered join.

Source code in src/symfonic/core/plugins/contribution.py
def render_position_group(
    contributions: list[PluginContribution],
    position: Position,
) -> str | None:
    """Render contributions at a single position into one joined string.

    Applies the header rule (§9 of the design): the FIRST contribution
    per ``(origin, position)`` gets an auto-header of
    ``### {origin}`` when its ``header`` is ``None``; subsequent
    contributions from the SAME origin at the SAME position emit raw
    content (no duplicate header). ``header=""`` always means "no
    header". A non-empty explicit ``header`` is emitted verbatim before
    the content.

    Empty-string ``content`` is skipped silently.

    Args:
        contributions: All contributions for the current turn (any
            position). The function filters to the requested ``position``
            internally.
        position: Which position group to render.

    Returns:
        Joined string for the position, or ``None`` when no contribution
        at this position produced renderable text. Returning ``None``
        (instead of ``""``) lets the engine drop the section cleanly
        from its layered join.
    """
    parts: list[str] = []
    seen_auto_header: set[str] = set()
    for contrib in contributions:
        if contrib.position != position:
            continue
        if not contrib.content:
            continue

        if contrib.header is None:
            origin = contrib.origin or "plugin"
            key = f"{origin}|{position}"
            if key in seen_auto_header:
                parts.append(contrib.content)
            else:
                parts.append(f"### {origin}\n{contrib.content}")
                seen_auto_header.add(key)
        elif contrib.header == "":
            parts.append(contrib.content)
        else:
            parts.append(f"{contrib.header}\n{contrib.content}")

    if not parts:
        return None
    return "\n\n".join(parts)