When a component leaves, its contribution is removed without residue — and without destroying unrelated work performed meanwhile.
Spatio-
temporal
Composability.
A programming paradigm for software that keeps changing while it runs — without leaving stale effects behind, and without breaking the components that depend on it.
Cordis.Production case study: Koishi, 4000+ community plugins.
Two independent guarantees.
Modern dynamically composed software needs two separate properties that neither implies the other.
When dependencies appear, disappear, or are replaced, consumers react coherently: activate only when satisfied, hold a stable resolution while running, deactivate before providers withdraw.
Symbolically written DΓ × PΓ × E*Γ. The paper connects these to the classical duals of effects and coeffects — but moves them from compile-time type reasoning into runtime mechanisms.
- The five contributions — revertible effects.
- The five contributions — reactive coeffects.
- The five contributions — a unified context paradigm.
- The five contributions — a dynamic composition calculus with system-level metatheory.
- The five contributions — a practical Cordis implementation with reconciliation & HMR.
Static is easy. Dynamic is the hard part.
Static systems resolve calls, imports, and inheritance before or at startup. Dynamic systems load, unload, and reconfigure pieces while continuing to run.
Supports dynamic extension install, but true fine-grained live unloading of arbitrary executable code is absent — a process restart is still the coarse boundary. Teardown lives in a separate place from setup, making cleanup a discipline, not a structure.
Directly relevant to ACRYL. Future harnesses may continuously generate and deploy modifications to themselves while still serving requests: tool suites, execution envs, permissions, session persistence, memory, subagent orchestration, UI.
OSes give temporal composability at process granularity; orchestrators give spatial composability at service granularity. But that is too coarse when composition happens inside one process — restarts destroy caches and partial work; services add network overhead.
Two halves of one duality.
Classical effect / coeffect theory supplies the vocabulary, but it is largely static. The paper reifies it into runtime-operable context objects.
Effects characterize what a computation does to the environment: resource allocation, event registration, timers, state mutation, service registration, dependency publication.
Coeffects characterize what a computation needs from the environment: permissions, resources, services, contextual data.
Type-level effect / coeffect metadata.
Turn it into an explicit, first-class object.
Operable when the dependency graph is live.
Every mutation pairs with its inverse.
Ordinary impure code is re-expressed as a pure transformation of an explicit context, then paired with a left inverse that recovers the prior state.
Because teardown must undo setup, the inverse of a composed operation is the reverse composition of its inverses.
/* twisted composition (4) */ (f₁, g₁) ∘ (f₂, g₂) := (f₁∘f₂, g₂∘g₁) /* effect context (5): state + recovery accumulator */ ∂Γ := Γ × (Γ → Γ) (γ, φ) — current state + prior-state recoverer /* tracking (6): extend the accumulator with the inverse */ track(f, g)(γ, φ) = ( f(γ), φ∘g ) /* recovery (9): run the accumulator, reset */ recover(γ, φ) = ( φ(γ), id )
A correctly tracked effect does not change what full recovery returns. For any sequence of tracked effects, recovery still returns the original state.
/* sequence recovery (11) */ recover( track(fₙ,gₙ) ∘ … ∘ track(f₁,g₁)(γ,φ) ) = recover(γ, φ) /* inverse stability — the deepest requirement */ φ(γ) = γ₀ or, later, observational: φ(γ) ≈ γ₀
The real difficulty is when two components interleave — an inverse may run after unrelated effects have moved the state. The paper requires independence between effects: their forward maps and every potential inverse must commute (18) and not change which inverse the other yields (19).
- Theorem 20 — under pairwise independence, one effect can be removed from an interleaved sequence, leaving exactly the state the sequence would have reached without it.
- Corollary 21 — under independence, inverses may be applied in any permutation and still recover the original state.
Declare needs; react coherently.
Dependency injection is formalized as a typed partial map, and publishing a dependency is itself a tracked revertible effect.
Σ := (k : K) ⇁ V_kDependency context: a typed partial map from keys to values.
σ ⊨ d ⟺ ∀k ∈ d : k ∈ dom(σ)Satisfaction — a component's requirement set is satisfied when every declared key is present.
notify_d(σ, σ′) := activating | deactivating | neutralEvery context transition is classified against the requirement set, driving activation or deactivation.
A logical key resolves to a realm, then the realm resolves to a value (27). Isolate the same key differently per subtree — per workspace, session, account, or sandbox.
/* (28) get through realm indirection */
get(k)(ρ, σ) = σ( ρ(k) )
isolate(k, r)(ρ, σ) = ( ρ[k↦r], σ )Metadata changes how a capability may be used rather than which provider satisfies it (29) — the basis of a capability-based access model.
/* (30) metadata-merged get */
get(k, μ)(ι, σ) = σ(k)( μ ⊕ₖ ι(k) )
intercept(k, ν)(ι, σ) = (ι[k↦ι(k)⊕ₖν], σ)The context is the mediated carrier.
Effects and coeffects unify into a recursive context that mediates both mutation and dependency access — and it makes recovery a derived global behavior, not per-component discipline.
Γ∞ := μΓ. Γ × (Γ → Γ) × ΣUnified context = recursive state × inverse accumulator × coeffect environment. The recursion lets a parent own child contexts and aggregate their removal.
Exact physical equality after rollback is often impossible or unnecessary. Freed memory need not return to the same layout; a fresh ID need not be numerically identical. What matters is that the recovered state is observationally indistinguishable through the context's operations.
Easy to reason about, but invasive — threading state through every call.
Ergonomic, but dependencies and effects are hidden.
An explicit first-class context mediates both mutation and dependency access.
A whole-system model of many interleaving components.
Section 3 proves local properties. Section 4 builds an operational semantics where fibers interleave, drain, reload, and fail.
A fiber is a live instantiation: ⟨d, p, e, π, σ, τ, θ⟩ — requirements, provisions, effect, parent, provided table, retirement flag, lifecycle state.
Not mounted, or failed.
Activation and setup in progress.
Current valid episode, holding a committed view.
Draining dependents, running inverses.
Runtime identity permanently gone.
(41) The target records exactly which provider identity should satisfy each requirement. Two providers may expose equal values yet be different lifecycles. Replacing a provider with a new instance therefore triggers a dependents' reload.
(42) Quiescence means: actual lifecycle state matches the resolved target state. The orchestrator only requests existence / retirement — it never directly forces a component active. Desired composition is input; activation state is derived.
Provider is serving consumers.
Enters UNLOADING — no longer advertised.
Dependents see new targets and begin their own teardown.
Only once consumers are done does the provider execute its inverse.
Gone, cleanly.
The provider's binding stays readable through each consumer's committed view while dependents drain — the implementation of Theorem 63.
Multi-step activation yielding an inverse per step (47), enabling partial rollback if interrupted mid-activation.
In-flight work is inertial — it must land, record its inverse, then chain into unload if the target became stale.
(49) A failure adds no new inverse, but earlier steps still recover — transactional activation, not a try/catch.
From runtime rules to formal guarantees.
The metatheory is where the paper converts plausible mechanisms into system-level guarantees that hold under interleaving.
Every rule preserves well-formedness (59): parents exist, provisions disjoint, committed views point to installed fibers.
(56–57) Removing one fiber leaves other fibers' independent work intact, and a closed episode leaves no observable residue.
(58–59) Provider activates before consumer; consumer drains before provider's inverse; no activation mixes provider versions.
(73) Same orchestration inputs yield the same quiescent observable state as a canonical from-scratch assembly.
| Result | Establishes | Runtime consequence |
|---|---|---|
| T20 | independent effect removable after interleaving | unrelated effects survive component removal |
| T40 | distinct-key operations are independent | fine-grained capability keys naturally commute |
| T59 | rules preserve well-formedness | runtime does not corrupt invariants |
| T61 / C62 | recovery exactness + terminal recovery | fiber removal leaves no observable residue |
| T63 / T64 | provider-consumer ordering + resolution coherence | dependents drain before provider inverse; no mixing |
| T66 | progress + termination | reconciliation reaches quiescence under acyclicity |
| T73 | confluence / canonical form | final state depends on final composition, not the valid schedule |
The theory, mapped almost literally to code.
Cordis turns the formalism into a TypeScript runtime: context, fibers, effects, injection, isolation, interception, a loader, and transactional HMR.
| Theory | Runtime |
|---|---|
| Γ∞ unified context | ctx |
| effect lifting | ctx.effect(callback) |
| Σ dependency map | ctx.get · ctx.set |
| isolation / interception | ctx.isolate · ctx.intercept |
| component instance | fiber |
| requirements d | fiber.inject |
| provisions p | component provide |
| activation effect e | fiber.apply |
| accumulator g | fiber.dispose |
| committed view ω | fiber.committed |
| target | fiber.target |
inverse = identity
for each successful effect step:
inverse = yieldedInverse ∘ inverse
return inverseThe armed guard makes disposal idempotent; child disposers compose into parents. Crucially, Cordis does not mechanically prove the inverse is correct — TypeScript trusts the author for that local obligation.
A provider in UNLOADING stops counting as available before its bindings are removed — dependents start teardown while still reading the old binding through their committed view.
reload may chain → unload
unload may chain → reloadConfig becomes the authoritative description of desired composition; reconciliation maps each change to the least disruptive fiber op.
change → rebuildRebuild the component.
→ unload / reloadOperational toggle.
→ realm reassignmentMove scope safely.
→ update / diffComponent-specific update.
Changed modules; external ones declined.
Walk dependency closure to declined boundaries.
Invalidate caches, dispose old, import new — and roll back on failure.
Unlike conventional HMR acceptance boundaries written by developers, Cordis can use fiber boundaries because every component's effects are already bounded and reversible.
Full applications built from these primitives, server-side and browser-console alike.
Community plugins; disabling/reloading preserves unrelated live state; unavailable dependencies leave consumers inactive rather than crashing.
What is not magic.
The discussion contains the most practically important caveats — and they matter most for an agent runtime.
The theory's Γ is bounded by what the runtime can exclusively control and restore.
open · malloc · forkTrackable — the handle can usually be released.
write · send · charge · emailOnce crossed the boundary, a true inverse may not exist.
Strategies: withholding (delay emission until local state commits) or compensation (delete / refund to restore application-level equivalence).
Dependency declarations form a capability-like model; interception attenuates it. But the paper is explicit: language-level mediation is not a sandbox against malicious code. If untrusted code can reach host objects directly it bypasses the context. Real isolation needs SFI, a separate runtime, a sandboxed process, or a VM/container/WebAssembly boundary.
Exclusive binding perturbs dependents on every switch. A stable broker stands the capability to consumers while providers come and go behind it — for rolling updates and load balancing.
Bare keys break under interface drift and key collision. Mitigate by namespacing, peer dependencies, or structural compatibility.
Dependency cycles leave components permanently inactive — detectable from declarations. Factor into smaller core + integration components, though bindings can grow quadratically.
Make context implicit while preserving semantics; compile effect iterators into state machines; make coeffect declarations part of the type system — enabling compile-time cycle detection and structural checks.
Make declared dependencies the component's entire reachable authority (as a WebAssembly module receives explicit imports), and make more resources natively revertible: transactional storage, copy-on-write, kernel-tracked acquisition.
A composition kernel for a live, self-modifying runtime.
The paper is not an agent runtime — it is a possible composition kernel. Translated, it argues for a much cleaner ACRYL boundary than wiring agent integrations into the product core.
# from Eq. (37), literally component: id: memory.hindsight requires: [ workspace.fs@1, persistence.sqlite@1 ] provides: [ memory.semantic@1 ] apply: - open database - start indexer - register memory provider # each atomic mutation yields its own disposer
The runtime should not accept a component as merely start() / stop(). It should mediate environmental effects so teardown is derived from setup.
The strongest translation of Eq. (31): ACRYL Context holds current hierarchical context, rollback accumulators, capability environment, session/task state, workspace identity, permissions, and child fiber contexts.
Session consumer holds committed view to Claude.
New targets stop resolving Claude.
Consumer target switches to Codex.
Consumer re-activates against one coherent new view.
Define capabilities like agent.execution@1, agent.streaming@1, agent.tool-use@1. A session component depends on them; the committed view ω records which live agent fiber provides each.
Hindsight · Supermemory · Mem0 · Honcho. Consumers depend only on the capability.
OmniGraph · lat.md. Isolation scopes the same logical key per workspace.
Agent synthesizes a component → LOADING fiber → each setup step yields an inverse → ACTIVE or rollback.
register command · enable tool · start subprocess · add watcher · mount UI
create remote issue → close. create cloud resource → destroy.
send email · post message · publish package · transfer money
Prefer to withhold irreversible emissions until the local composition has committed.
capability: workspace.fs@1 policy: read: [ /repo/** ] write: [ /repo/src/** ] deny: [ /repo/.env ]
Policy is supplied by the context, not the provider. But per Section 6.3 this is not a sandbox for hostile code.
acryl://core/memory.semantic/v1 acryl://core/agent.execution/v2 acryl://omnigraph/code.graph/v1
A public capability protocol should not rely on a bare string key. Encode namespace + version + contract, and ultimately want a structural compatibility check.
Build the precedence graph (60) before activation and detect cycles — so two plugins aren't silently inactive. The UI can explain the cycle and suggest a decomposition into session-core + bindings.
Not every capability needs exclusive binding. A stable broker survives backing-provider rolling updates without reloading every consumer each time one worker changes.
Adopt directly. Do not copy blindly.
The useful ideas are separable from the exact TypeScript implementation. Some adapt cleanly; several warnings are non-negotiable.
- Effect ledger / inverse-first mutation API — every mutation returns its disposer.
- Capability declarations — every component gets explicit
requiresandprovides. - Provider identity in target state — track which instance a consumer committed against.
- Four-state lifecycle — inactive / loading / active / unloading, plus failure.
- Drain dependents before provider recovery — a kernel-level rule.
- Iterated setup with rollback checkpoints — especially for self-generated extensions.
- Desired-state reconciliation — the orchestrator changes config; the runtime derives transitions.
- Transactional HMR / self-update — keep old code until the new version instantiates.
- Context realms — for workspaces, rooms, accounts, sessions, sandboxes.
- Interception metadata — as a generic policy / capability attenuation.
- Do not assume every effect is invertible — the paper itself does not.
- Do not make bare key equality the public compatibility protocol.
- Do not treat context-level capability access as a security sandbox.
- Do not force the whole application into Cordis before prototyping.
- Do not ignore independence assumptions — prefer fine-grained capability ownership over one global bag.
A minimal acryl-compose kernel workspace.fs agent.execution → OpenCodeProvider · PiProvider memory.semantic → HindsightProvider · SupermemoryProvider graph.code → OmniGraphProvider · LatProvider
- Load a provider, start a consumer.
- Replace the provider while the consumer is active.
- Verify the consumer drains before the old provider disappears.
- Inject a failure mid-activation; verify restore.
- Mutate two independent providers concurrently.
- Unload one; verify the other's state remains.
- Isolate the same capability into two workspaces.
- Apply permission interception to one realm only.
spatial: consumer sees one stable provider view
progress: reconciliation reaches quiescence
confluence: same desired output ⇒ same observable state
Each criterion mirrors a formal guarantee from the metatheory.
Every component declares what it needs, what it provides, and how every environmental mutation can be withdrawn; the runtime owns the composition semantics. That moves a system from a collection of adapters and lifecycle callbacks toward a genuine live composition runtime.
Persist the scene / context; make every actor, provider, tool, memory system, graph system and extension a reconciled fiber whose effects are bounded and whose dependencies are explicit.