stateDiagram-v2 [*] --> Inactive Inactive --> Active: dependencies satisfied Active --> Inactive: dependency disappears Active --> Active: dependency binding changes / refresh Inactive --> [*]
Reactive Coeffects: Dependencies That Control Component Lifetime
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
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 = NoneWe 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