symfonic.core.prompt.blocks.sources.computed¶
computed ¶
ComputedBlockSource -- a prompt block the host computes per call.
ENVIRONMENT is the block this exists for. "You are running in
production, region eu-west-1, against the live billing API" is a
deployment fact that is known at runtime and stale the moment it is
pinned in a config literal. This adapter hands the decision back to the
host: it calls a supplied callable with (scope, block_id) and takes
back (content, revision).
It is the one built-in adapter that can legitimately serve different
content per tenant. That matters more than it looks: without it, every
scope-aware path in the block layer -- scope_path keying, the
per-tenant branch of the resolver, REQ-SUBAGENT's scope propagation --
would have nothing to exercise it until a database adapter shipped, and
a code path with no honest caller is a code path that is wrong by the
time it gets one. A host closure over a dict is enough to keep those
paths tested for real.
The host owns the revision, and therefore owns the cache¶
The callable returns the revision alongside the content, rather than this adapter hashing what came back. Hashing here would look safer and would quietly be worse: content that is semantically unchanged but textually noisy -- a timestamp in the rendered text, a dict iterated in a new order -- would hash differently on every turn and re-bill the cached prefix each time. Only the host knows whether its content moved in a way that matters, so only the host can name the revision.
The corollary is a contract the host must honour: the same revision must mean the same content, within a scope. A host that returns a constant revision for content that changes will serve stale text from every cache keyed on it. This adapter cannot detect that -- it never sees a second call's content next to the first -- so it is stated here rather than implied.
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.StorageErrorthe resolver routes through the block'son_source_failurepolicy 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:TypeErrorand not fromStorageError, so it is not absorbed by the failure policy. Routing it there would mean a learned block whose policy isomitdisappears 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.
Threading¶
A synchronous callable is run with :func:asyncio.to_thread rather than
called inline. Host code behind this interface routinely does blocking
I/O -- a DB query, a metadata HTTP call -- and running that on the event
loop thread would stall every other in-flight turn in the process, a
failure that shows up as unexplained tail latency rather than as an
error anyone traces back here. An async def callable is awaited
directly, on the loop, as its author intended.
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
¶
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.
ComputedBlockSource ¶
ComputedBlockSource(
compute: BlockComputer,
*,
offline_safe: bool = False,
timeout: float | None = None,
)
Serves one prompt block by calling a host-supplied callable.
Satisfies :class:~symfonic.core.prompt.blocks.protocol.BlockSource
structurally and stops there -- see below for why history is not
presented.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
compute
|
BlockComputer
|
Called as |
required |
offline_safe
|
bool
|
Host-declared, defaulting to |
False
|
timeout
|
float | None
|
Seconds to wait for |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
scope_aware |
bool
|
Always |
Why there is no history: a callable computes a value, it does not
retain the values it computed. list_revisions / load_revision
are therefore not presented, isinstance(src,
HistoryCapableBlockSource) is False, and
:func:~symfonic.core.prompt.blocks.validation.check_operator_editable
rejects operator_editable=True against it at construction. A host
whose backing store does keep history should ship an adapter that
presents it, not declare it through this one.
Source code in src/symfonic/core/prompt/blocks/sources/computed.py
current_revision
async
¶
Return the current revision id without building a revision object.
Not part of :class:BlockSource; offered for parity with
:class:~symfonic.core.prompt.blocks.sources.static.StaticBlockSource
and :class:~symfonic.core.prompt.blocks.sources.file.FileBlockSource
so a cache-validity check has one shape across adapters. Unlike
those two, this is not cheap here: it calls compute in
full, the same as :meth:load, because a computed value has no
separate metadata read -- the callable's return value is the
only source of the revision. A caller doing a cache-validity check
against a computed-backed block pays the full call, not a
shortcut.
Source code in src/symfonic/core/prompt/blocks/sources/computed.py
load
async
¶
Compute the current revision of block_id for scope.
Both arguments are passed through to the host callable verbatim, so content may differ per tenant and per block.
Raises:
| Type | Description |
|---|---|
ComputedBlockUnavailableError
|
The callable raised. Routed
through the block's |
ComputedBlockContractError
|
The callable returned something
other than a |
Source code in src/symfonic/core/prompt/blocks/sources/computed.py
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
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.