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:
ContributionSourcekeeps working, and the synchronous :func:~.compiler.compile_promptkeeps compiling them without an event loop; - a source whose content lives behind an
await-- the shape everysymfonic.core.prompt.blockssource already has, whereBlockSource.loadis a coroutine functionPromptBlockResolverawaits -- declaresareadand 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_coroutine_read ¶
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
is_sync_source ¶
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.