Skip to content

symfonic.capabilities.prompting.ports

ports

The source port: what a contribution is asked, and what it may answer.

Split out of :mod:.contracts when S01 gave the port its asynchronous half and the module passed its 300-line budget. The line the split follows is a real one rather than a convenience: :mod:.contracts declares what a contributor states about its content -- tier, layer, scope, ordering, failure policy -- and this module declares the two-sided conversation with whatever holds the bytes.

Two protocols, one meaning. ContributionSource.read is called and AsyncContributionSource.aread is awaited; a source declares one, or both, and :func:is_sync_source / :func:is_async_source are how the compiler's two doors decide which of them can serve it. What a source must never be is neither: :meth:~.contracts.PromptContribution.validate refuses that at declaration, because a source nothing can ask is a contribution that would be accepted, validated, and never resolved.

AsyncContributionSource

Bases: Protocol

The asynchronous half of the same port (S01, TA8.51).

read stays exactly what it was. This is a second member rather than a coroutine version of the first, and the choice is the whole design:

  • every source written against :class:ContributionSource keeps working, and the synchronous :func:~.compiler.compile_prompt keeps compiling them without an event loop;
  • a source whose content lives behind an await -- the shape every symfonic.core.prompt.blocks source already has, where BlockSource.load is a coroutine function PromptBlockResolver awaits -- declares aread and is awaited by :func:~.compiler.compile_prompt_async.

A synchronous bridge was the other option and it was refused on purpose: asyncio.run inside read explodes on a loop that is already running, and a thread hop per contribution per turn would trade a missing port for a latency defect on every turn that has nothing to do with blocks.

A source may declare both. Declaring both means the synchronous door can still compile it; declaring only aread means the synchronous door refuses it by name rather than dropping it silently.

ContributionScope

Bases: StrEnum

How widely one contribution's content is shared.

ContributionSource

Bases: Protocol

Reads the current content of one contribution for one scope.

scope_aware and offline_safe are declared members, so an isinstance check requires them to be present: a source that never decided whether it keys on scope does not satisfy this protocol.

SourceRead dataclass

SourceRead(text: str, revision: str = '', untrusted: bool = False, fields: Mapping[str, str] | None = None)

What a source answered.

untrusted is the source's own declaration about its payload. A source that fetches a web page or reads a user-writable row says so here, and the render gate then refuses to place it at an authored tier.

SourceRequest dataclass

SourceRequest(contribution_id: str, scope_path: str = '', turn: int = 0, scope: ContributionScope = ContributionScope.DEPLOYMENT)

What a source is asked for: one contribution, in one scope, on one turn.

StaticSource dataclass

StaticSource(text: str, revision: str = 'static', untrusted: bool = False, scope_aware: bool = False, offline_safe: bool = True)

A source whose content is fixed at declaration time.

Deployment-global by construction: scope_aware is False because one literal string is the same for every tenant, and saying so is what lets the scope-pairing check reject a tenant-scoped block backed by it.

is_async_source

is_async_source(source: object) -> bool

True when source offers the awaited member.

Source code in src/symfonic/capabilities/prompting/ports.py
def is_async_source(source: object) -> bool:
    """``True`` when ``source`` offers the awaited member."""
    return callable(getattr(source, "aread", None))

is_coroutine_read

is_coroutine_read(source: object) -> bool

True when source wrote async def read instead of aread.

The mistake is one keystroke from the intended shape and it used to be invisible: :func:is_sync_source matched on callable(read) alone, so such a source passed as synchronous, resolve_source_async called it without awaiting, and the coroutine object travelled into the renderer as if it were a :class:SourceRead. The failure surfaced as an AttributeError on .revision two modules from its cause, plus a "coroutine was never awaited" warning.

The mirrored mistake -- aread only, taken to the synchronous door -- has always been refused by name. This makes the pair symmetric.

Source code in src/symfonic/capabilities/prompting/ports.py
def is_coroutine_read(source: object) -> bool:
    """``True`` when ``source`` wrote ``async def read`` instead of ``aread``.

    The mistake is one keystroke from the intended shape and it used to be
    invisible: :func:`is_sync_source` matched on ``callable(read)`` alone, so
    such a source passed as synchronous, ``resolve_source_async`` called it
    without awaiting, and the coroutine object travelled into the renderer as
    if it were a :class:`SourceRead`. The failure surfaced as an
    ``AttributeError`` on ``.revision`` two modules from its cause, plus a
    "coroutine was never awaited" warning.

    The mirrored mistake -- ``aread`` only, taken to the synchronous door -- has
    always been refused by name. This makes the pair symmetric.
    """
    return inspect.iscoroutinefunction(getattr(source, "read", None))

is_sync_source

is_sync_source(source: object) -> bool

True when source offers the synchronous member.

A coroutine read is deliberately not one: it cannot be called on the synchronous door and calling it on the asynchronous one would produce a coroutine where the compiler expects bytes. See :func:is_coroutine_read.

Source code in src/symfonic/capabilities/prompting/ports.py
def is_sync_source(source: object) -> bool:
    """``True`` when ``source`` offers the synchronous member.

    A coroutine ``read`` is deliberately **not** one: it cannot be called on
    the synchronous door and calling it on the asynchronous one would produce a
    coroutine where the compiler expects bytes. See :func:`is_coroutine_read`.
    """
    return callable(getattr(source, "read", None)) and not is_coroutine_read(source)