Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. consume B through initialize exactly once;
  2. completely interpret initialization Actions before accepting mailbox events;
  3. process at most one accepted B::Event at a time;
  4. call transition exactly once for that event;
  5. if the fold succeeds, interpret the returned Actions exactly once; and
  6. commit its Continue, Goto, or Stop verdict 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:

  1. 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;
  2. each committed child's initialization effects once before public publication or ordinary ingress;
  3. every named sends lane in its structural InterpretSends order; and
  4. 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 matching C::Event control authority;
  • atomically commit the creator-local protocol-occurrence/CreationId binding;
  • report EstablishedCreation<C, Occurrence>::Installed after 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:

RequestAdapter 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.