Reactive Coeffects: räumliche Abhängigkeitsreaktion
Das Papier verwendet Reactive Coeffects, um die spatial composability zu lösen — ob das System Lebenszyklen und Abhängigkeitsverschaltung automatisch anpassen kann, nachdem die Abhängigkeiten einer Komponente erscheinen, verschwinden oder ersetzt werden. Kernidee: Komponenten deklarieren, was sie benötigen (eine Coeffect-Spezifikation), und die Laufzeit benachrichtigt sie bei Kontextänderung gemäß Spezifikation.
Coeffect Context und "set ist ein Effect"
[Originale Schlussfolgerung des Papiers §3.2.1] Der Coeffect Context Σ ≔ (k : K) ⇀ 𝒱_k ist eine endliche partielle Funktion von Abhängigkeitsschlüsseln auf getypte Werte. get und set sind die beiden zentralen Operationen (Def 23), und set(k, v) ist selbst ein Effect — eine Coeffect-Operation ist ein Effect, daher erbt die Coeffect-Bereitstellung automatisch Trackability und Recoverability. Das ist die Brücke zwischen Revertible Effects und Reactive Coeffects: eine Abhängigkeit deklarieren (provide) ist zugleich ein revertibler Effect.
Coeffect Specification und der Drei-Zustands-Wechsel
[Originale Schlussfolgerung des Papiers §3.2.2] Eine Coeffect-Spezifikation d ⊆ K ist die Menge der Abhängigkeitsschlüssel, die eine Komponente deklariert; das Satisfaction Predicate σ ⊧ d ≔ ∀k ∈ d. k ∈ dom(σ). Jeder Effect, der σ in σ' überführt, lässt sich nach d als activating / deactivating / neutral (Def 26) klassifizieren:
- activating: vor der Änderung erfüllte σ d nicht, nach der Änderung erfüllt σ' d — die Komponente sollte aktiviert werden;
- deactivating: vor der Änderung erfüllt, nach der Änderung nicht mehr — die Komponente sollte entladen werden;
- neutral: vor und nach der Änderung erfüllt, oder vor und nach nicht erfüllt — die Komponente muss den Zustand nicht wechseln (aber die Abhängigkeitsinstanz kann ausgetauscht worden sein).
Vollständiges Beispiel Datenbank A + RAG B
Konkreter Fall (das Standardbeispiel durch das ganze Papier):
Plugin A (Datenbank-Plugin): set('database', dbService) stellt Abhängigkeit bereit
Plugin B (RAG-Plugin): deklariert d_B = {'database'} braucht die Abhängigkeit- A erscheint: A stellt den Datenbankdienst über
ctx.set('database', dbService)bereit; der Schlüssel'database'erscheint in σ. Das von B deklarierted_B = {'database'}geht von nicht erfüllt zu erfüllt — activating, B wird benachrichtigt und automatisch aktiviert und erhält dbService ausctx.get('database'). - A verschwindet oder wird ersetzt: A's Fiber tritt in Unloading ein; der Schlüssel
'database'wird aus σ entfernt. B'sd_Bgeht von erfüllt zu nicht erfüllt — deactivating, B wird benachrichtigt und automatisch entladen (sein eigener Inverse Accumulator wird angewendet und gibt die von B gehaltenen Ressourcen frei). - Ein neues A ist bereit: das neue Datenbank-Plugin A' stellt über
ctx.set('database', newDbService)bereit. B'sd_Bgeht wieder von nicht erfüllt zu erfüllt — activating, B reaktiviert sich mit der neuen Abhängigkeit und erhält newDbService.
Im gesamten Prozess muss B's Code nicht aktiv prüfen "falls A weg ist, dann ..." — die Laufzeit benachrichtigt automatisch gemäß der Coeffect-Spezifikation.
Committed View vs Target View
[Originale Schlussfolgerung des Papiers §4.1, §4.2] In der Cordis-Implementierung gibt es zwei zentrale Abhängigkeitsauflösungs-Sichten:
- Committed View (ω): die aktuell committete Abhängigkeitsauflösung, die das Mapping von Fiber-Namen auf Provider aufzeichnet. Was eine Komponente während eines Episode liest, ist diese View; sie garantiert, dass Abhängigkeiten während des aktuellen Lebenszyklus der Komponente stabil sind (siehe das Ordering-Theorem).
- Target View: der Abhängigkeitsauflösungszustand, in dem sich das Fiber befinden sollte, von
refreshbei Konfigurationsänderung neu berechnet. Wenn Target und Committed divergieren, werden Lebenszyklusübergänge (Aktivierung, Entladung oder Reload) ausgelöst.
[Originale Schlussfolgerung des Papiers §4.2] Wichtiges Detail: die Target View speichert Provider-Identity (Fiber-UID), nicht Wertgleichheit. Das Papier stellt ausdrücklich fest: "makes the comparison usable, since a different fiber providing an equal value would otherwise compare equal" — würden nur Values verglichen, würden verschiedene Fibers, die denselben Value liefern, fälschlich als "Abhängigkeit hat sich nicht geändert" beurteilt, die Reaktivierung würde nicht ausgelöst, und der Consumer würde weiterhin einen Snapshot des alten Fibers verwenden, ohne zu wissen, dass der Provider getauscht wurde. fiber.target speichert in der Cordis-Implementierung einen UID-Digest des Abhängigkeitsauflösungsergebnisses (§5.1.3).
Isolation und Interception
[Originale Schlussfolgerung des Papiers §3.2.3] Zwei orthogonale abgeleitete Mechanismen:
- Isolation: über die Isolation-Realm-Tabelle
ρ : K ⇀ Rlöst sich derselbe Key k in verschiedenen Kontexten zu verschiedenen Realms r auf und bindet somit an verschiedene Werte.ctx.isolate(k, r)leitet einen neuen Kontext ab, der nur das Mapping von ρ an k überschreibt (eine Derived Realization, ohne die Eltern-Tabelle zu verändern). - Interception: hängt Metadaten an einem Coeffect
ι : K → ℙ_k; die Provider-Funktion empfängt die Metadaten und passt ihr Verhalten entsprechend an.ctx.intercept(k, ν)leitet einen Kontext ab, der ν in die Metadaten an k einfügt.
Unterschied: Isolation ersetzt "zu welchem Binding ein Key auflöst" (Instanzwechsel), Interception verändert "wie das Binding verwendet wird" (Constraints hinzufügen, kein Instanzwechsel). Isolation ist eine Derived Realization (kein Inverse nötig — einfach den Kind-Kontext verwerfen), während set eine In-Place Realization ist (ein Inverse ist nötig, um rückgängig zu machen).
Szenarienvergleich
| Szenario | Isolation verwenden | Interception verwenden |
|---|---|---|
| Multi-Tenant / verschiedene Workspaces | Derselbe Key 'db' löst sich pro Tenant zu eigener Instanz auf | — |
| Unabhängige Umgebung für Sub-Agenten | Dasselbe 'filesystem' löst sich pro Sub-Agent zu unabhängiger Instanz auf | — |
| Test-Doubles | Mit einem Mock-Realm überschreiben | — |
| Schreibgeschützte Dateisystemberechtigung | — | Readonly-Metadatum anhängen |
| Verschiedene Plugins nutzen verschiedene Modelle | 'model'-Key jeweils isolieren | — |
| Strenge Zugriffsrichtlinie für Community-Plugins | — | Capability-Beschränkungs-Metadatum anhängen |
Reactive Coeffects lösen "Wahrnehmen von Abhängigkeitsänderungen", aber der Lade-/Entlade-/Reload-Prozess der Komponenteninstanz selbst braucht weiterhin eine Zustandsmaschine. Siehe Lebenszyklus und Entladereihenfolge.