Behavior adapter contract
An adapter drives one concrete B: Behavior. It owns execution and resource
capabilities while the behavior owns state and semantic decisions.
Universal execution law
For each actor incarnation an adapter must:
- consume
Bthroughinitializeexactly once; - completely interpret initialization
Actionsbefore accepting mailbox events; - process at most one accepted
B::Eventat a time; - call
transitionexactly once for that event; - if the fold succeeds, interpret the returned
Actionsexactly once; and - commit its
Continue,Goto, orStopverdict exactly once.
A controlled fold error commits no partial actions. Cancellation or adapter failure must not manufacture a successful semantic fact.
Static boundary
The adapter is monomorphized over the complete behavior and its capabilities:
B: Behavior
B::Sends: InterpretSends<Adapter, B::Event, Here>
B::Birth::Child: DispatchBirth<B::Protocol::Addr, Adapter>
The concrete bounds vary with the host design, but every lane and child
alternative must have a compile-time implementation. A universal driver may
be generic; it may not erase events, effects, endpoints, futures, or child
types to achieve universality. EndpointAddress itself does not require every
endpoint to be Send. A thread-safe driver instead requires Send on the
concrete Actions, request, event, endpoint, or future that it actually moves
across its asynchronous boundary.
Actions commitment
For one successful Actions value, the adapter interprets:
- creations in vector order, reserving a fresh unpublished address, running each child's pure initialization fold, then privately committing its host, exact endpoint, and creator-local binding when those steps succeed;
- each committed child's initialization effects once before public publication or ordinary ingress;
- every named sends lane in its structural
InterpretSendsorder; and - the next-behavior or termination verdict.
All creation attempts precede sends and interpreter requests from the same action. This is a Bombay policy that makes create-then-send and create-then-observe deterministic. It is not a general actor-model ordering guarantee between independent actors.
Within SendLayer, inner effects are interpreted before wrapper-owned effects.
Within a vector lane, values retain vector order. The algebra deliberately
does not invent an order between independent product lanes beyond the product's
own InterpretSends implementation.
An expected rejection retains its complete request and does not suppress independent later effects. Interpreter corruption retains the settled prefix, faulting value, and untouched suffix. Neither outcome can be reinterpreted as successful readiness, restart, or observation. A child whose private host was already committed remains an established birth even when its initialization effects later fail; it drains without public publication.
Fresh creation
CreateChild<A, C> contains a CreationId that is non-reused within its exact
static child occurrence, a concrete child, and CreationKind. The adapter must
not derive an address from the creation ID. Equal numeric IDs belonging to
different occurrences remain different correlations.
For each ordered creation batch and routed request it must:
- prepare every required runtime route without partially consuming the batch;
- allocate an address fresh with respect to the actor configuration;
- initialize the concrete child exactly once;
- install a private runtime host, exact endpoint for
C::Protocol, and matchingC::Eventcontrol authority; - atomically commit the creator-local protocol-occurrence/
CreationIdbinding; - report
EstablishedCreation<C, Occurrence>::Installedafter that commitment, without claiming initialization-effect success; and - interpret the child's initialization actions once, then make a continuing successful child publicly resolvable before ordinary ingress.
Failure before host commitment publishes no capability. Failure after private
commitment preserves the established birth and its exact action settlement but
does not publish the endpoint to logical resolution. After ownership transfer,
InitializationRejected returns the current child and exact error, while
InitializationPanicked returns the extant current child after a caught pure
initialization panic, and
HostRejected returns the current child, uninterpreted initialization actions,
and matching CreationRejection. Before transfer, rejection returns the
original creation batch with ChildNamespaceExhausted. An allocation collision
is CreationRejection::Allocation(AddressAlreadyClaimed) when the selected
allocator rejects it; it can never mean replacement.
DispatchBirth recursively selects the concrete child. One
EstablishChild<Occurrence, C> implementation is required per structural
occurrence. Position is interpreter navigation evidence, not hosting
identity.
When the adapter retains exact child bindings, it may derive their complete
direct-child product through ChildOccurrenceProduct. Behavior owns and seals
the leaf, ChildChoice, and Never recursion; the adapter's
ChildOccurrenceShape chooses only its storage constructor. The product is
compile-time structure and performs
none of the creation transaction above. Each installed child derives its own
product from its own Behavior::Birth, so creator namespaces are not flattened
or shared.
When an effect carries a nominal occurrence, require
Emitter: ResolveChildOccurrence<Occurrence> and select the mapper-owned slot
with its associated Position. This sealed projection accepts generated roles,
raw ChildHead/ChildTail<_> paths, and genuinely transparent wrappers. It
rejects inherited roles when a wrapper changes protocol or birth topology; an
adapter must not repair that mismatch with a registry or runtime search.
Destination interpretation
The three delivery paths have different obligations:
| Request | Adapter action |
|---|---|
Delivery<P> | resolve Recipient<P>::address() under the runtime's logical-name policy, then deliver P::Msg |
ChildDelivery<P, O> | resolve the current creator's committed (O, nonce) binding, then deliver |
EstablishedDelivery<P> | transfer the exact endpoint and deliver directly |
The adapter must preserve P statically throughout. It may not coalesce two
protocols merely because their addresses or message layouts match.
EndpointAddress lets the runtime's own address newtype select the endpoint
family. InterpretEstablished, InterpretEstablishedDelivery, and the exact
observation/shutdown interpreter traits are public power-user boundaries. They
do not grant endpoint access outside the explicit transfer call, but they are
not sealed as exclusive runtime authority.
Event injection
Interpreter-originated facts return through their declared
ReturnsToEmitter<Input, Path>. The adapter retains the corresponding
Ingress<Input, Path> or equivalent static constructor and enqueues exactly
the root B::Event it produces.
Ancestor reports instead carry their destination ingress in the request and
declare NoReturnToEmitter. For ReportShutdownPlan<P, Path>, the adapter
calls into_event and enqueues that exact root event. It must not deliver the
plan to the emitting inner lane, mutate the coordinator directly, or treat the
request as successful installation before the coordinator consumes the event.
It must not inspect payload types to find a lane. Repeated fact types at different wrapper depths are distinct because their structural paths are distinct.
Observation
Exact observation uses ObservationId as observer-local relationship
correlation and the supplied endpoint as incarnation identity. The adapter
must return a complete EstablishedObservation<P> fact for start, cancel,
rejection, or eventual stop. Duplicate live IDs and cancellation of a missing
relationship are explicit rejections.
Address-based ObservePeer<A> remains separate. A missing live address is not
proof that the requested incarnation stopped. Without a selected live
incarnation or authoritative retained terminal fact, the adapter returns its
own error rather than fabricating PeerStopped.
Orderly shutdown
ShutdownEstablished<B, Path> supplies an exact endpoint and a typed
Ingress<ShutdownRequested, Path>. The adapter injects that event through the
normal mailbox boundary. It returns EstablishedShutdownResolved<P>::Accepted
only when the request is admitted, or the precise rejection otherwise.
The same attempt returns the generic action settlement: accepted leaves the
exact ShutdownId, while AlreadyStopping or AlreadyStopped returns the
complete ShutdownEstablished request. The concrete shutdown port returns only
Result<(), ShutdownRejection> and cannot substitute another settlement
shape.
Acceptance is not termination. Eventual termination is reported through the observation relationship.
Environment and liveness
Clock reads, timer queues, bounded-mailbox waiting, task scheduling, endpoint storage, networking, persistence, and operating-system failures are adapter concerns. They must enter or leave the fold only through declared typed events, effects, and errors.
Backpressure may delay a concrete interpretation future. The adapter must not silently drop or reorder accepted effects. Fairness, scheduling policy, and resource limits are runtime policy and should be documented by the runtime, not presented as laws of this crate.
Conformance checklist
- initialization once and before mailbox events;
- one event at a time and one fold per accepted event;
- creations before dependent sends/requests;
- fresh allocation independent of nonce;
- rejected creation commits no binding or endpoint;
- static interpretation for every effect and child occurrence;
- exact endpoints bypass logical-name resolution;
- all returning facts use their declared structural ingress;
- ancestor reports use only the explicit ingress capability carried by the request;
- no erased envelopes, registries, downcasts, or hidden side effects; and
- no success, restart, stop, or observation fact is inferred from failed mechanics.
Exact observation ownership
The exact observation interpreter consumes the whole ObserveEstablished<P>
or CancelObservation<P> request. It must preserve that original on rejection,
commit the issued exact relationship before publishing Started, and consume a
successful cancellation grant into its nonauthorizing relationship receipt.
Stopped retains exact relationship, outcome and notification timestamp without
retaining cancellation permission. Completion decision and cancellation share
one membership owner; a reused numeric ID cannot authorize an old grant. A
custom interpreter is a trusted issuance boundary, not a compiler-proven
registration service. See Established capabilities
for requests constructed outside folds, original input emission, serial
never-accepted retry, both legal control arrival orders, and retirement
admission custody.