Guide

Providers and Requirement Closure

Carry exact Resource requirements through child charts and close them with one dominating authored provider.

Composition should preserve authority, not hide it. A reusable child chart may know that an Activity needs a model pool without knowing which application, tenant, or ancestor will provide one. Motive keeps that need in the chart declaration until authored topology can prove one exact provider for the consumer's whole possible lifetime.

That proof is requirement closure. Requirements travel out through child boundaries while they remain open. Once a compatible provider dominates the consumer, a precise provider capability travels back down with each child birth. Nothing is recovered from a global registry or whichever service happens to be nearest at runtime.

A requirement is part of the chart

Every Resource.Input or Resource.Output placement contributes a structured requirement to the chart definition. It records four facts together:

ResourceModelPool

The authored capability tag and its exact Schema contract.

PlacementInput

Whether the consumer acquires a value or creates a durable key.

ConsumerWorking.CompletePrompt

The occurrence path that needs the capability.

StatusUnresolved

The chart boundary has not yet proved one provider.

The result is inspectable declaration data, not an inferred Effect environment:

LeafChart.definition.resourceRequirements;
// [{
//   _tag: "Unresolved",
//   placement: "Input",
//   resource: "ModelPool",
//   contract: /* exact Resource contract */,
//   consumer: ["Working.CompletePrompt"],
// }]

An engine may register this open chart so another chart can spawn it. Starting it directly as a root is different: the engine refuses with ResourceRequirementsNotClosed, including the same structured requirement summary. Registration preserves composition; root start requires closure.

Closure is a topology proof

The provider does not have to be textually close to the Activity. It must be structurally guaranteed to remain active whenever the consumer can be active. In the example, Application.Running mounts both ModelPools and the child that eventually reaches CompletePrompt; the provider therefore dominates the entire descendant lifetime.

Read the example in both directions. The first two frames carry one open requirement from LeafChart through IntermediateChart. The final frame adds the dominating provider, closes the root summary, and pins that capability through both child boundaries.

Requirement closure

Requirements travel up. Provider capabilities travel down.

Watch one exact ModelPool.Input requirement cross two child boundaries, then close when Root.Running supplies its sole dominating provider.

1 · Open leaf requirement

The leaf tells the truth about what it needs.

CompletePrompt consumes ModelPool.Input, but LeafChart mounts no provider. Statechart.make preserves one Unresolved requirement at Working.CompletePrompt; an engine may register the chart for composition, but refuses to start it as a root.

import { Activity, Resource, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";

export class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}

const ModelPool = Resource.make("ModelPool", {
  key: Schema.String,
  resource: Schema.Struct({ model: Schema.String }),
});

const CompletePrompt = Activity.make("CompletePrompt", {
  input: Schema.Struct({ pool: ModelPool.Input }),
  success: Schema.Void,
});

export class Leaf extends State.Compound<Leaf>()("Leaf") {
  static states = States.make(() => [Idle, Working]);
}

class Idle extends State.Atomic<Idle>()("Idle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Working),
  ]);
}

class Working extends State.Atomic<Working>()("Working") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(CompletePrompt, () => ({ pool: ModelPool.acquire("default") })),
    on([CompletePrompt.Done, CompletePrompt.Error]),
  ]);
}

export const LeafChart = Statechart.make(Leaf);

export const leafRequirements = LeafChart.definition.resourceRequirements;

2 · Child composition

Each child boundary preserves the open requirement.

IntermediateChart does not inspect or restate the leaf Activity. spawn(LeafChart) lifts the same requirement to Delegating.Leaf › Working.CompletePrompt, preserving its placement, contract, and complete consumer coordinate.

import { Activity, Resource, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";

export class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}

const ModelPool = Resource.make("ModelPool", {
  key: Schema.String,
  resource: Schema.Struct({ model: Schema.String }),
});

const CompletePrompt = Activity.make("CompletePrompt", {
  input: Schema.Struct({ pool: ModelPool.Input }),
  success: Schema.Void,
});

class Leaf extends State.Compound<Leaf>()("Leaf") {
  static states = States.make(() => [LeafIdle, Working]);
}

class LeafIdle extends State.Atomic<LeafIdle>()("LeafIdle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Working),
  ]);
}

class Working extends State.Atomic<Working>()("Working") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(CompletePrompt, () => ({ pool: ModelPool.acquire("default") })),
    on([CompletePrompt.Done, CompletePrompt.Error]),
  ]);
}

export const LeafChart = Statechart.make(Leaf);

export class Intermediate extends State.Compound<Intermediate>()("Intermediate") {
  static states = States.make(() => [Idle, Delegating]);
}

class Idle extends State.Atomic<Idle>()("Idle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Delegating),
  ]);
}

class Delegating extends State.Atomic<Delegating>()("Delegating") {
  static transitions = Transitions.make(this, ({ spawn }) => [
    spawn(LeafChart),
  ]);
}

export const IntermediateChart = Statechart.make(Intermediate);

export const intermediateRequirements = IntermediateChart.definition.resourceRequirements;

3 · Dominating provider

One authored provider closes the descendant requirement.

Application.Running mounts ModelPools beside IntermediateChart. Because that provider is guaranteed to remain active for the descendant's whole lifetime, the root summary closes and the exact capability is delegated through both child births.

import { Activity, Resource, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";

export class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}

const ModelPool = Resource.make("ModelPool", {
  key: Schema.String,
  resource: Schema.Struct({ model: Schema.String }),
});

const CompletePrompt = Activity.make("CompletePrompt", {
  input: Schema.Struct({ pool: ModelPool.Input }),
  success: Schema.Void,
});

class Leaf extends State.Compound<Leaf>()("Leaf") {
  static states = States.make(() => [LeafIdle, Working]);
}

class LeafIdle extends State.Atomic<LeafIdle>()("LeafIdle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Working),
  ]);
}

class Working extends State.Atomic<Working>()("Working") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(CompletePrompt, () => ({ pool: ModelPool.acquire("default") })),
    on([CompletePrompt.Done, CompletePrompt.Error]),
  ]);
}

export const LeafChart = Statechart.make(Leaf);
const ModelPools = ModelPool.as("ModelPools");

export class Application extends State.Compound<Application>()("Application") {
  static states = States.make(() => [Idle, Running]);
}

class Idle extends State.Atomic<Idle>()("Idle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Running),
  ]);
}

class Running extends State.Atomic<Running>()("Running") {
  static transitions = Transitions.make(this, ({ provide, spawn }) => [
    provide(ModelPools),
    spawn(IntermediateChart),
  ]);
}

class Intermediate extends State.Compound<Intermediate>()("Intermediate") {
  static states = States.make(() => [IntermediateIdle, Delegating]);
}

class IntermediateIdle extends State.Atomic<IntermediateIdle>()("IntermediateIdle") {
  static transitions = Transitions.make(this, ({ on }) => [
    on(Begin, Delegating),
  ]);
}

class Delegating extends State.Atomic<Delegating>()("Delegating") {
  static transitions = Transitions.make(this, ({ spawn }) => [
    spawn(LeafChart),
  ]);
}

export const IntermediateChart = Statechart.make(Intermediate);

export const RootChart = Statechart.make(Application);

export const rootRequirements = RootChart.definition.resourceRequirements;

Children preserve open requirements

Spawning LeafChart does not ask IntermediateChart to restate the leaf's Activity or translate its dependency into a service name. The child mount retains the child's original requirement, and the intermediate chart publishes a longer consumer coordinate:

LeafChart          Working.CompletePrompt
IntermediateChart  Delegating.Leaf › Working.CompletePrompt

Each segment is an authored occurrence in one chart. This is why repeated placements, keyed families, and deeper descendants remain distinguishable: composition extends an exact path instead of flattening every need into ModelPool.

The same rule continues across any number of ordinary child boundaries. Recursive charts use a finite directional requirement form rather than pretending an unbounded descendant path can be enumerated; the closure rule itself remains the same.

One dominating provider closes the requirement

A compatible candidate closes an open requirement only when it is the sole dominating provider. Motive checks authored coactivity, the Resource tag, the complete contract, and the placement direction.

  • A provider in a mutually exclusive sibling is only possibly active, so the requirement stays Unresolved.
  • Two dominating compatible providers produce Ambiguous; ancestor composition does not silently choose one.
  • A provider with the same tag but a different Schema contract is incompatible and cannot close the requirement.
  • One dominating provider still wins when another candidate is only possibly active; a partial competitor does not create false ambiguity.

These outcomes are stable declaration facts. They do not depend on registration order, Layer merge order, or which process happens to answer first.

Closure becomes durable delegation

When RootChart closes the requirement, its root summary becomes empty. The proof is not then discarded. The direct child mount records a ResourceDelegation containing the placement, Resource contract, remaining child-relative consumer path, and exact provider occurrence.

At runtime that delegation is copied into the child's immutable birth record. When the intermediate child creates the leaf, it forwards the same provider capability while removing the path segment it has crossed. A recovered runner therefore resolves the provider chosen by authored topology; it does not repeat ambient discovery.

This is the bridge between a static composition proof and durable execution. The model decides which authority a child may receive. The engine preserves that decision across process boundaries, recovery, and redelivery.

Test the closure boundary

  1. Inspect the leaf and intermediate definitions and verify their requirements remain structured and open at the correct consumer paths.
  2. Start either open chart as a root and verify ResourceRequirementsNotClosed returns that exact summary.
  3. Add one dominating compatible provider and verify the root requirement list becomes empty and the child mount gains one delegation.
  4. Move the provider into a mutually exclusive sibling and verify the requirement becomes Unresolved again.
  5. Mount two dominating providers and verify Ambiguous preserves both candidate paths.
  6. Change the provider's key, Resource, material, or failure contract under the same tag and verify it no longer closes the consumer.
  7. Encode and decode a child birth record and verify the pinned delegation survives exactly.

Next, Delegating Resource Authority with Attenuation narrows that pinned capability at a child boundary, so composition can grant the required Resource without granting every key the parent is allowed to use.