Skip to content

symfonic.core.prompt.blocks.sources.computed_contract

computed_contract

The contract a computed block's host callable must satisfy.

The type of the callable, the two failure types it can produce, and the three checks that decide which of them a given wrong answer is. Split out of :mod:symfonic.core.prompt.blocks.sources.computed so the adapter module holds the adapter and this one holds the rules it enforces.

Failures are typed; contract violations are not

The two ways a computed source can go wrong are not the same kind of event, and they are deliberately not the same kind of exception:

  • The callable raised -- the database was down, the metadata service timed out. That is environmental and transient, so it arrives as :class:ComputedBlockUnavailableError, a :class:~symfonic.core.protocols.StorageError the resolver routes through the block's on_source_failure policy exactly like an unreadable file.
  • The callable returned the wrong shape -- a bare string, a 3-tuple, a blank revision. That is a bug in host code. It is deterministic, it will recur on every call, and it will never heal. It raises :class:ComputedBlockContractError, which derives from :class:TypeError and not from StorageError, so it is not absorbed by the failure policy. Routing it there would mean a learned block whose policy is omit disappears from every prompt, silently, for as long as the bug lives -- the exact months-later silent failure the block layer's construction-time checks exist to prevent. A loud failure on the first turn costs minutes; a silent one costs a quarter.

A :class:~symfonic.core.protocols.StorageError raised by the host itself passes through unwrapped: a host that already typed its failure (NotFoundError for a tenant with no row) knows more about it than this adapter does, and re-wrapping would flatten that distinction.

BlockComputer module-attribute

BlockComputer: TypeAlias = Callable[[TenantScope, str], ComputedBlockResult | Awaitable[ComputedBlockResult]]

The host-supplied callable. Sync or async def; both are accepted.

ComputedBlockResult module-attribute

ComputedBlockResult: TypeAlias = tuple[str, str]

What a host callable returns: (content, revision), in that order.

ComputedBlockContractError

Bases: TypeError

The host callable returned something that is not (content, revision).

Deliberately a :class:TypeError and not a :class:~symfonic.core.protocols.StorageError: this is a deterministic bug in host code, not an outage. It must not be swallowed by an omit failure policy, because a block that vanishes silently from every prompt is discovered months later, if at all. See the module docstring.

ComputedBlockUnavailableError

Bases: StorageError

The host callable raised while computing the block.

Typed as a :class:~symfonic.core.protocols.StorageError so the resolver routes it through the block's on_source_failure policy instead of letting an arbitrary exception from host code escape past it and fail a turn whose policy said to omit the block.

reject_wrong_arity

reject_wrong_arity(compute: BlockComputer) -> None

Raise :class:ComputedBlockContractError if compute cannot take (scope, block_id).

Checked at construction, where a mis-wired callable is a config mistake at the site that supplied it, rather than at the first call, where the TypeError Python raises for a bad signature is indistinguishable from a real outage to :meth:ComputedBlockSource.load's except Exception -- see the module docstring.

Some callables (certain builtins, some C extension types) have no introspectable signature at all; :func:inspect.signature raises ValueError for those, and the mismatch -- if any -- is left to surface at call time rather than blocking construction of something this function cannot actually check.

Source code in src/symfonic/core/prompt/blocks/sources/computed_contract.py
def reject_wrong_arity(compute: BlockComputer) -> None:
    """Raise :class:`ComputedBlockContractError` if ``compute`` cannot take ``(scope, block_id)``.

    Checked at construction, where a mis-wired callable is a config
    mistake at the site that supplied it, rather than at the first call,
    where the ``TypeError`` Python raises for a bad signature is
    indistinguishable from a real outage to :meth:`ComputedBlockSource.load`'s
    ``except Exception`` -- see the module docstring.

    Some callables (certain builtins, some C extension types) have no
    introspectable signature at all; :func:`inspect.signature` raises
    ``ValueError`` for those, and the mismatch -- if any -- is left to
    surface at call time rather than blocking construction of something
    this function cannot actually check.
    """
    try:
        signature = inspect.signature(compute)
    except (TypeError, ValueError):
        return
    try:
        signature.bind(_SCOPE_PLACEHOLDER, "")
    except TypeError as exc:
        raise ComputedBlockContractError(
            f"the compute callable {compute!r} must accept (scope, block_id); binding "
            f"a call with two positional arguments raised {exc}. This is a wiring "
            "mistake in host code, not a per-call outage, so it fails at construction "
            "rather than surfacing as ComputedBlockUnavailableError on the first turn "
            "that resolves the block"
        ) from exc

require_pair

require_pair(raw: Any, *, scope: TenantScope, block_id: str) -> ComputedBlockResult

Return raw as a validated (content, revision) pair.

Source code in src/symfonic/core/prompt/blocks/sources/computed_contract.py
def require_pair(
    raw: Any, *, scope: TenantScope, block_id: str
) -> ComputedBlockResult:
    """Return ``raw`` as a validated ``(content, revision)`` pair."""
    where = f"block {block_id!r} at scope_path {scope.scope_path!r}"
    if not isinstance(raw, tuple) or len(raw) != 2:
        raise ComputedBlockContractError(
            f"the compute callable for {where} must return a 2-tuple of "
            f"(content, revision); got {type(raw).__name__} "
            f"{'of length ' + str(len(raw)) if isinstance(raw, tuple) else ''}"
            ". Returning the content alone leaves nothing to key the prompt "
            "cache on"
        )
    content, revision = raw
    if not isinstance(content, str) or not isinstance(revision, str):
        raise ComputedBlockContractError(
            f"the compute callable for {where} must return (str, str); got "
            f"({type(content).__name__}, {type(revision).__name__}). The "
            "content is rendered into the prompt verbatim, so it is not "
            "coerced here"
        )
    if not revision.strip():
        raise ComputedBlockContractError(
            f"the compute callable for {where} returned a blank revision. The "
            "revision is what every prompt cache keys on; a blank one cannot "
            "distinguish this content from the next value the callable "
            "produces. Return a hash, a version or a monotonic id"
        )
    return content, revision

returns_awaitable

returns_awaitable(compute: BlockComputer) -> bool

Return whether compute must be awaited rather than threaded.

Checks the callable itself (covering plain async def and functools.partial of one) and then its __call__, so a class instance implementing async def __call__ is recognised too. A sync function that returns a coroutine is not detected here and does not need to be -- :meth:ComputedBlockSource.load awaits whatever it gets back.

Source code in src/symfonic/core/prompt/blocks/sources/computed_contract.py
def returns_awaitable(compute: BlockComputer) -> bool:
    """Return whether ``compute`` must be awaited rather than threaded.

    Checks the callable itself (covering plain ``async def`` and
    ``functools.partial`` of one) and then its ``__call__``, so a class
    instance implementing ``async def __call__`` is recognised too. A
    sync function that *returns* a coroutine is not detected here and
    does not need to be -- :meth:`ComputedBlockSource.load` awaits
    whatever it gets back.
    """
    if inspect.iscoroutinefunction(compute):
        return True
    # Looked up on the *type*, which is where Python finds a dunder for
    # implicit invocation, and statically, so a callable with a dynamic
    # ``__getattr__`` is not invoked merely by being inspected.
    call = inspect.getattr_static(type(compute), "__call__", None)
    return call is not None and inspect.iscoroutinefunction(call)