Reactive Coeffects: Dependencies That Control Component Lifetime

Agents
Systems
Distributed Systems
How components declare what they need and react when runtime dependencies change.
Author

Chirag Sehra

Published

August 17, 2026

Effects Ask: What Did I Change?

Coeffects Ask: What Do I Need?

Consider this agent component:

class AgentLoop:
    async def run(self, ctx):
        response = await ctx.services["llm"].complete(...)
        await ctx.services["sessions"].append(response)

It depends on:

llm
sessions

It may also require:

tools
approvals
sandbox

Those requirements are currently hidden inside implementation code.

The runtime discovers them when something fails:

KeyError: "llm"

A better model makes them explicit:

class AgentLoop:
    requires = {
        "llm",
        "sessions",
        "tools",
    }

Now the runtime can reason about dependencies before executing the component.

That is the intuition behind the paper’s reactive coeffects.(Cordiverse 2026)


The Coeffect Context

The paper models the dependency environment as:

\Sigma \coloneqq (k:K)\rightharpoonup \mathcal{V}_k

Think of \Sigma as a typed partial table:

key         value

llm         DeepSeekAdapter
tools       ToolRegistry
sessions    PostgresSessionStore

“Partial” matters because a key can be absent:

llm         DeepSeekAdapter
sessions    PostgresSessionStore
tools       MISSING

A dependency specification can then be tested against the current environment.

Conceptually:

\sigma \models d

means that state \sigma satisfies dependency specification d.

For a basic list of required services, satisfaction means all required keys are currently resolvable.(Cordiverse 2026)


Example: A Reactive Dependency Runtime

class Runtime:
    def __init__(self):
        self.services = {}
        self.components = []

    def provide(self, name, value):
        self.services[name] = value
        self.refresh()

    def remove(self, name):
        del self.services[name]
        self.refresh()

    def refresh(self):
        for component in self.components:
            component.refresh(self)

Define a component:

class Component:
    def __init__(self, requires, apply):
        self.requires = set(requires)
        self.apply = apply
        self.scope = None

    def satisfied(self, runtime):
        return self.requires <= runtime.services.keys()

Now dependency availability is explicit.


Activation and Deactivation

stateDiagram-v2
  [*] --> Inactive
  Inactive --> Active: dependencies satisfied
  Active --> Inactive: dependency disappears
  Active --> Active: dependency binding changes / refresh
  Inactive --> [*]

def refresh(self, runtime):
    ready = self.satisfied(runtime)

    if ready and self.scope is None:
        self.activate(runtime)

    elif not ready and self.scope is not None:
        self.deactivate()

Activation:

def activate(self, runtime):
    self.scope = EffectScope()
    self.apply(runtime, self.scope)

Deactivation:

def deactivate(self):
    self.scope.dispose()
    self.scope = None

We now have:

dependencies missing
        │
        ▼
     INACTIVE
        │
 dependencies satisfied
        ▼
      ACTIVE
        │
 dependency disappears
        ▼
     INACTIVE

Effects and coeffects have started to work together.


Example: Component Lifecycle

Suppose the agent requires:

agent = Component(
    requires={"llm", "tools", "sessions"},
    apply=start_agent,
)

At startup:

services = {}
Agent: INACTIVE

Add sessions:

runtime.provide("sessions", sessions)

State:

sessions ✓
llm      ✗
tools    ✗

Agent: INACTIVE

Add LLM:

runtime.provide("llm", deepseek)

State:

sessions ✓
llm      ✓
tools    ✗

Agent: INACTIVE

Add tools:

runtime.provide("tools", tool_registry)

Now:

sessions ✓
llm      ✓
tools    ✓

Agent: ACTIVE

Remove tools:

runtime.remove("tools")

Now:

Agent: INACTIVE

The dependency relation drives lifetime.

The agent does not need to poll:

while True:
    if tools_exist():
        ...

Three Interesting Context Changes

Activating

before:
llm   ✓
tools ✗

after:
llm   ✓
tools ✓

Satisfaction changes:

false → true

The component can activate.

Deactivating

before:
llm   ✓
tools ✓

after:
llm   ✓
tools ✗

Satisfaction changes:

true → false

The component must deactivate.

Still satisfied, but different

before:
llm = Provider A
tools ✓

after:
llm = Provider B
tools ✓

The dependency predicate remains:

true → true

But the resolved dependency changed.

This case becomes the central problem of Part 4.


Relatable Example: Linux Supplier and Consumer Devices

Linux device links provide a strong mental model here.

A dependency can be represented as:

Supplier
   │
   ▼
Consumer

The kernel can use this information to enforce lifecycle ordering such as supplier availability before consumer probe and consumer unbinding before supplier removal.(The Linux kernel developers n.d.)

Compare:

GPU/MMU Supplier
        │
        ▼
Consumer Device

with:

LLM Provider
     │
     ▼
Agent Loop

or:

MCP Connection
      │
      ▼
MCP Tool Plugin

The useful intuition is:

A consumer should not be operational while a required provider is unavailable.

Limits of the Comparison

Linux device links are not a general effect/coeffect calculus.

Cordis combines dependency-driven lifetimes with reversible effects, context scoping, replacement and runtime composition.

The analogy explains why dependencies should participate in lifecycle, but it does not capture the paper’s full model.


Dependency Graphs Instead of Boot Order

Without declared dependencies, startup often becomes:

load_sessions()
load_tools()
load_llm()
load_agent()

Why this order?

Because someone knows:

agent requires llm
agent requires tools
agent requires sessions

The knowledge is encoded as an ordered script.

With many plugins, boot order becomes fragile:

plugin 17 before plugin 34
plugin 29 after plugin 8
plugin 57 only if plugin 13 exists
...

Reactive dependencies instead encode the graph:

Sessions ─────────────┐
                     │
Tools ────────────────┼──► Agent
                     │
LLM ─────────────────┘

The runtime can derive lifecycle behaviour from the graph.

The distinction is:

"start these things in this order"

versus:

"this component requires these capabilities"

Cordis Mapping: inject

Cordis uses declared service dependencies to control plugin activation. DeepSeek Harness builds on that mechanism and also uses package/runtime invariants around mutable services and lifecycle relationships.(DeepSeek AI 2026; DeepSeek Harness contributors 2026)

Conceptually:

inject = ['llm', 'tools', 'sessions']

means:

I consume:
llm
tools
sessions

not:

run me after lines 5, 9 and 14 of boot.ts

Our Python:

requires={"llm", "tools", "sessions"}

teaches the architectural idea without pretending to be Cordis syntax or implementation.


Dependency Cycles

Suppose:

A requires B
B requires A

Initial state:

A inactive because B absent
B inactive because A absent

Nothing starts.

This is not the same as a traditional mutex deadlock, but it has a similar practical failure mode: a dependency cycle prevents progress.

A common architectural response is decomposition.

Instead of:

Agent requires ToolRegistry
ToolRegistry requires Agent

split the integration:

AgentCore
ToolRegistryCore

ToolExecutionBridge
    requires AgentCore
    requires ToolRegistryCore

The dependency relation becomes acyclic.


Previous: Revertible Effects: Making Runtime Changes Undoable · Next: Async Reloads, Epochs and the Race You Probably Missed

References

Cordiverse. 2026. A Programming Paradigm for Spatiotemporal Composability. https://github.com/cordiverse/paper.
DeepSeek AI. 2026. DeepSeek Harness. https://github.com/deepseek-ai/deepseek-harness.
DeepSeek Harness contributors. 2026. Meaningful Package Invariant Contracts. https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md.
The Linux kernel developers. n.d. Device Links. https://docs.kernel.org/driver-api/device_link.html.