symfonic.kernel.contracts.contributions¶
contributions ¶
What a capability hands the compiler — W2 phase 2.
Until now CompileRequest.capabilities was a sequence of names, recorded
for diagnostics, while the stages themselves were assembled into the request by
hand. That works and is what the extension-seam probe does, but it leaves the
two halves of a capability — what it declares and what runs it — in different
places, joined by nothing a compiler can check.
A :class:CapabilityContribution is both halves in one value. That is what
makes the rule enforceable:
**Every stage a contribution declares must be answered by a handler in the
same contribution.**
Scoped to contributions on purpose. The same rule applied to every compiled stage was written, landed and reverted within an hour, because it made the existing hand-assembled path uncompilable — all 17 probe tests failed on legitimate use of a shipped API. A rule that breaks working callers to prevent a defect they do not have is a worse rule than the defect. Applied here it costs nobody anything: a contribution is new, and a capability that declares a stage it cannot run has simply not finished.
The second invariant is the one T2.3.1 wrote as STG-8 and nothing enforced: a
contribution's effect_grants must cover the union of its stages' declared
effects. A capability that declares an effect it was not granted is refused at
compile rather than at the point of the effect.
Neither invariant makes a capability useful. Both make a specific way of being useless impossible to express, which is the only kind of guarantee this codebase has learned to trust.
CapabilityConfig ¶
Bases: Protocol
The seam Agent(capabilities=[...]) accepts.
One method, because a capability's whole job at compile time is to answer "given these grants, what do you contribute?". Anything it needs to do happens in its handlers, at dispatch, where the kernel can bound it.
CapabilityContribution
dataclass
¶
CapabilityContribution(capability: str, stages: tuple[StageDescriptor, ...] = (), handlers: Mapping[str, Any] = (lambda: MappingProxyType({}))(), effect_grants: frozenset[str] = frozenset(), tools: tuple[Any, ...] = (), preconditions: tuple[Any, ...] = ())
One capability's declaration and the code that answers it.
validate ¶
Refuse the shapes that compile and cannot work, or must not.
Raised at fold time so the offending capability is named, rather than surfacing later as a stage nothing answers or an effect nobody granted.
Source code in src/symfonic/kernel/contracts/contributions.py
CapabilityRequest
dataclass
¶
CapabilityRequest(effect_grants: frozenset[str] = frozenset(), options: Mapping[str, Any] = (lambda: MappingProxyType({}))())
What a capability is told before it decides what to contribute.
Deliberately small. A capability that needs to inspect the whole compile request to decide its stages is one whose contribution depends on another capability's, and that is an ordering problem the stage ladder already solves — not something to solve again by widening this.
fold_contributions ¶
fold_contributions(configs: Sequence[CapabilityConfig], *, effect_grants: frozenset[str] = frozenset(), options: Mapping[str, Any] | None = None) -> tuple[tuple[StageDescriptor, ...], dict[str, Any], frozenset[str], tuple[tuple[str, Any], ...], tuple[str, ...], tuple[Any, ...]]
Collect every contribution into the pieces a CompileRequest needs.
Returns (stages, handlers, grants, tools, capability_names,
preconditions), where each tool is paired with the name of the capability
that offered it.
Preconditions accumulate in contribution order and none replaces another: they are checks on one call, the first objection wins, and a capability that could drop a peer's check would be granting itself a bypass.
The pairing is attribution the fold can produce and the caller cannot: two
capabilities offering the same tool name has to be refused by name of
capability, and a composition root holding a flat tuple of tool objects
can only report the collision as "two tools called run_agent" — true,
and useless to whoever has to fix it. The kernel reads no attribute of the
tool to do this, so it still interprets nothing.
Two capabilities claiming the same stage_id is refused here rather than
resolved by ordering: whichever won would depend on registration order, and
a capability whose stage silently never runs is the failure this whole phase
exists to make impossible.
Source code in src/symfonic/kernel/contracts/contributions.py
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |