Skip to content

Incremental composition

0.3 could answer what composition was resolved, and why. 0.4 answers what actually changed between two generations, what must be rebuilt because of that, and what can be safely reused. Composition becomes incremental: a change rebuilds only the nodes whose semantic inputs changed, and a node that did not change is the exact same runtime instance in both generations.

This page is the mental model. The guarantees are in design.md; the change list is in CHANGELOG.md.

One change, one rebuild

Start from a published generation with a root provider, a telemetry exporter, a finance provider, and a research provider at version 7:

gen42
root
├── postgres
├── telemetry
├── finance
└── research:v7

Now publish research at version 8 — same entry, same scope, same configuration, new implementation. 0.4 reports:

postgres   reused     semantic identity unchanged
telemetry  reused     semantic identity unchanged
finance    reused     semantic identity unchanged
research   rebuilt    implementation changed

Only research was replaced. postgres, telemetry, and finance are the same runtime instances they were in gen42, and a run that still holds gen42 keeps observing them exactly as before. If a consumer depended on research, it would be rebuilt too — see dependency-driven rebuild.

Semantic identity

Every composition node — a mounted plugin instance, keyed by its entry id — has a semantic identity: the set of inputs that can change what the node does.

Input Why it participates
implementation the plugin implementation (manifest identity plus the concrete class)
capability contracts provides, requires, optional, permissions, config version
configuration the effective configuration; a credential change must force a rebuild
scope path moving an entry changes which providers it may see
dependency bindings the provider each requirement resolved to, as an entry and as a runtime instance

Two identities compare equal only when every input above is equal. That equality is the reuse proof: if Chassis reuses a node, its observable behaviour cannot have changed.

What is deliberately not part of identity: generation ids, sequence numbers, timestamps, plugin metadata, scope metadata values, and a preference that selects the same provider it already selected. These do not change behaviour, so they never force a rebuild.

The inputs are kept as separate fingerprints rather than one opaque hash, so a diagnostic can say why:

node.implementation_fingerprint
node.contract_fingerprint
node.config_fingerprint        # opaque; derived from unredacted config, never emitted
node.dependency_fingerprint

SemanticIdentity.semantic_id is a displayable digest built from non-secret structure only. The private fingerprints never leave the control plane: a credential change must be detected, not reported. See Semantic sameness is not physical reuse.

Dependency-driven rebuild

Impact follows real dependency bindings, not scope membership. If a consumer's selected provider changes, the consumer is rebuilt — it captured its capability objects during setup, and leaving them in place would point at a provider that is about to be disposed. This is reported distinctly:

research-provider   rebuilt    config changed
research-retriever  rewired    dependency research-provider changed

rewired means the node's own implementation, contracts, configuration, and scope are unchanged, and only what it resolves to moved. A dependency change also propagates transitively along the actual graph, so a provider deep in the graph rebuilds exactly the nodes that reach it.

A scope change narrows impact, it does not define it: a change inside /tenant:acme/research does not rebuild /tenant:globex/research unless there is a real dependency edge between them. Unrelated sibling scopes are reused.

Reason vocabulary

Every rebuild reports one or more of:

config_changed
implementation_changed
dependency_changed
scope_visibility_changed
provider_selection_changed
capability_contract_changed
preference_changed

Structural sharing and reachability lifetime

Reuse is structural sharing: two live generations reference the same runtime instance.

                    postgres
                  /        \
             gen42          gen43

Sharing is never shared mutability. A reused instance is never reconfigured in place — if a change would require mutating it, the node is rebuilt instead. A published generation therefore still never observes a composition change after publication.

A shared resource stays alive while any live generation can reach it. Disposal is unchanged from earlier releases (reuse did not add a second lifetime system): the generation still drains its leases, and the resource is disposed only once no live generation reaches it. Generation pressure now reports that reachability:

report = harness.diagnostics.generation_pressure()
resource = next(item for item in report.resources if item.entry_id == "postgres")
resource.generations     # ("gen_0044", "gen_0043", "gen_0042")
resource.retained_by     # ("lease", "sharing")

lease means a run still holds a generation that reaches the resource; sharing means more than one live generation reaches it, so no single generation's retirement would release it.

Stateful resources

Some runtime resources are inherently stateful — DB pools, HTTP clients, caches, model clients, telemetry exporters. Statefulness alone does not forbid reuse. What matters is whether the resource's behavioural contract is unchanged: the same pool configuration, credential source, endpoint, and lifecycle semantics reuse safely, because they are part of the node's configuration fingerprint. A changed credential or endpoint changes that fingerprint and forces a rebuild.

Chassis makes no assumption about third-party mutability it cannot enforce. A resource that is reconfigured in place is not reused: a change is a new instance, and the old one is shared only until no live generation reaches it. If a plugin mutates shared state outside the inputs Chassis can see, that is the plugin's contract to keep, not something structural sharing will paper over.

Reuse safety is inferred from semantic identity; 0.4 adds no opt-in or opt-out policy, because the conservative default is already the safe one: anything Chassis cannot prove safe is rebuilt.

Semantic sameness is not physical reuse

These are two different facts, and 0.4 keeps both explicit:

  • semantically unchanged — the node's identity is equal; reuse would be safe.
  • physically reused — the exact same lifecycle-managed instance was retained.

They normally coincide. They can differ: replacing an entry with a semantically identical implementation forces a new revision, hence a new instance, without changing behaviour. That is reported as UNCHANGED (semantically identical) rather than REUSED (which is only ever claimed when the instance really was retained). The runtime snapshot makes the same distinction:

snapshot.semantic_digest()   # composition identity; equal across separately built equivalents
snapshot.physical_digest()   # the runtime instance ids
snapshot.digest()            # the whole record

Asking why

old, new = gen42.generation_id, gen43.generation_id

harness.diagnostics.diff_generations(old, new)          # structural diff + a NODES section
harness.diagnostics.analyze_impact(old, new)            # ImpactAnalysis over every node
harness.diagnostics.explain_reuse(old, new, "research") # ReuseExplanation for one node

A ReuseExplanation carries the decision, the reasons, the changed inputs, the dependency that changed, and the shared instance id:

explained = harness.diagnostics.explain_reuse(old, new, "research")
explained.decision            # "rebuilt"
explained.reasons             # ("implementation_changed",)
explained.changed_inputs      # ("implementation",)
explained.shared_instance_id  # None
explained.to_dict()           # structured, JSON-compatible

ReconcileResult.impact carries the same analysis for the reconciliation that just ran, and harness.diagnostics.analyze_impact recomputes it from any two published generations.

Agent revisions

An agent revision is a composition change like any other, so the same rules apply: a revision that leaves a contribution unchanged does not reinstall its entry, and the mounted instance is carried across the revision change. A revision that adds a contribution mounts only that node; a revision that changes a scope's capability or tool view narrows or widens visibility and rebuilds exactly the consumers whose resolved bindings moved. Unrelated agent scopes are reused, and a shared provider is retained.

Rollback

Candidate construction is still transactional. New nodes are mounted before anything is published; reused nodes are shared, never re-created. If building a new node fails:

  • every candidate-owned mount is rolled back;
  • reused nodes from older generations are untouched;
  • the current generation remains current;
  • no partial candidate becomes visible.

Limitations

  • Reuse is conservative. Anything Chassis cannot prove safe is rebuilt: a changed revision rebuilds even when the new node is semantically identical.
  • The semantic digest reflects redacted configuration, so a secret-only change is invisible in snapshot.semantic_digest(). It still forces a rebuild, because the private identity fingerprint is computed from the effective configuration.
  • There is no content-addressed build cache, no cross-process sharing, and no persistent composition graph. Sharing is per process and follows generation reachability.
  • Identity is a proof of observable equivalence for inputs Chassis can see. A plugin that reads a file, clock, or remote endpoint at setup time can still behave differently after reuse; Chassis does not virtualize external systems.