Back to Blog

Rule Engine Design: Forward-Chain Reasoning Principles and Implementation

Forward chaining is the core reasoning mode of coomia-dip's ReasoningEngine. This article dives deep into the Rete network data structure design, Alpha/Beta node matching algorithms, conflict resolution strategies, and incremental fact update mechanisms. Through complete code implementations and performance benchmarks, we demonstrate how to achieve matching and firing of thousands of rules within millisecond-level latency.

CoomiaPublished on August 24, 202517 min read
Share this articleTwitter / X

Series: S5 Intelligent Decisions · Article 2 | Level: Advanced | Reading Time: 20 min

Rule Engine Design: Forward-Chain Reasoning Principles and Implementation

#TL;DR

Forward chaining is the core reasoning mode of coomia-dip's ReasoningEngine. This article dives deep into the Rete network data structure design, Alpha/Beta node matching algorithms, conflict resolution strategies, and incremental fact update mechanisms. Through complete code implementations and performance benchmarks, we demonstrate how to achieve matching and firing of thousands of rules within millisecond-level latency.

#1. Forward-Chain Reasoning Fundamentals

#1.1 What Is Forward-Chain Reasoning

Forward chaining is a data-driven reasoning approach: starting from known facts, matching rule conditions to fire rules that produce new facts, iterating until no new facts are generated.

Code
Forward-Chain Reasoning Flow:

Initial Fact Set          Rule Base              Derived Facts
+------------+      +------------+      +------------+
| Fact A     |      | Rule 1:    |      | Fact D     |
| Fact B     |----->| A ^ B -> D |----->| Fact E     |
| Fact C     |      | Rule 2:    |      | Fact F     |
|            |      | D ^ C -> E |      |            |
|            |      | Rule 3:    |      |            |
|            |      | B ^ E -> F |      |            |
+------------+      +------------+      +------------+

Cycle 1: Facts {A,B,C} -> Rule 1 fires -> adds D
Cycle 2: Facts {A,B,C,D} -> Rule 2 fires -> adds E
Cycle 3: Facts {A,B,C,D,E} -> Rule 3 fires -> adds F
Cycle 4: No new rules match -> HALT

#1.2 Comparison with Backward Chaining

FeatureForward ChainingBackward Chaining
DriverData-drivenGoal-driven
Starting PointKnown factsTarget to prove
Search DirectionConditions to conclusionsConclusions to conditions
Use CaseMonitoring, real-time alertsDiagnosis, query answering
Computation CostMay produce unused derivationsFocused on target
coomia-dip EngineReasoningEngineNot yet implemented

coomia-dip chose forward chaining as the primary reasoning mode because enterprise decision scenarios are predominantly event-driven -- when new data arrives, all relevant rules need automatic evaluation.

#1.3 The Naive Approach and Its Problems

A brute-force approach checks every rule against every fact combination each cycle:

Python
# Naive forward chaining - O(R * F^C) per cycle
def naive_forward_chain(facts: set, rules: list[Rule]) -> set:
    """
    R = number of rules
    F = number of facts
    C = max conditions per rule
    Each cycle: O(R * F^C) -- completely impractical at scale
    """
    changed = True
    while changed:
        changed = False
        for rule in rules:
            for combo in itertools.product(facts, repeat=len(rule.conditions)):
                if rule.matches(combo):
                    new_fact = rule.fire(combo)
                    if new_fact not in facts:
                        facts.add(new_fact)
                        changed = True
    return facts

With 1,000 rules and 10,000 facts, this produces over 10^12 comparisons per cycle. The Rete algorithm solves this.

#2. Rete Network Architecture

#2.1 Core Concept

Rete (Latin for "net") was proposed by Charles Forgy in 1979. Its core principle is trading space for time:

Code
Rete Network Structure:

                         Root Node
                        /    |    \
                       /     |     \
               +------+ +------+ +------+
               |Alpha | |Alpha | |Alpha |
               |Node 1| |Node 2| |Node 3|
               | A>10 | | B="X"| | C<5  |
               +--+---+ +--+---+ +--+---+
                  |        |        |
              Alpha     Alpha    Alpha
              Memory    Memory   Memory
                  |        |        |
                  |   +----+----+   |
                  +-->|  Beta   |<--+
                      | Node 1  |
                      | A.id =  |
                      | B.ref   |
                      +----+----+
                           |
                      Beta Memory
                           |
                      +----+----+
                      |Terminal |
                      | Node    |
                      |(Rule 1) |
                      +---------+

Two key optimizations:

  1. Temporal Redundancy: Between cycles, most facts remain unchanged. Rete only processes incremental changes.
  2. Structural Similarity: Rules share common conditions. Shared Alpha/Beta nodes avoid redundant evaluation.

#2.2 Node Types

Code
Node Type Hierarchy:

  ReteNode (abstract)
      |
      +-- RootNode            -- entry point, receives all facts
      |
      +-- AlphaNode           -- single-fact condition test
      |     |
      |     +-- AlphaMemory   -- stores facts passing alpha test
      |
      +-- BetaNode            -- joins two inputs (alpha or beta)
      |     |
      |     +-- BetaMemory    -- stores partial matches (tokens)
      |
      +-- TerminalNode        -- represents a fully matched rule

#2.3 coomia-dip Rete Implementation

Python
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable
from enum import Enum
import time


class FactType(str, Enum):
    """Supported fact types in coomia-dip Ontology"""
    OBJECT = "object"
    EVENT = "event"
    METRIC = "metric"
    RELATION = "relation"


@dataclass(frozen=True)
class Fact:
    """Immutable fact in working memory"""
    fact_id: str
    fact_type: FactType
    attributes: tuple[tuple[str, Any], ...]  # frozen for hashing
    timestamp: float = field(default_factory=time.time)

    def get(self, attr: str) -> Any:
        for k, v in self.attributes:
            if k == attr:
                return v
        return None


@dataclass
class Token:
    """Partial match -- a list of facts that jointly satisfy conditions so far"""
    facts: list[Fact]
    bindings: dict[str, Any] = field(default_factory=dict)

    def extend(self, fact: Fact, new_bindings: dict[str, Any]) -> Token:
        return Token(
            facts=self.facts + [fact],
            bindings={**self.bindings, **new_bindings},
        )

#3. Alpha Network: Single-Fact Filtering

#3.1 Alpha Node Design

Each Alpha node tests one condition against a single fact. Facts that pass flow into the Alpha Memory.

Python
@dataclass
class AlphaNode:
    """Tests a single condition on one fact"""
    node_id: str
    fact_type: FactType
    attribute: str
    operator: str          # "eq", "gt", "lt", "gte", "lte", "in", "regex"
    value: Any
    memory: list[Fact] = field(default_factory=list)
    children: list[BetaNode] = field(default_factory=list)

    # Precompiled test function for speed
    _test_fn: Callable[[Any], bool] | None = field(default=None, repr=False)

    def __post_init__(self):
        self._test_fn = self._compile_test()

    def _compile_test(self) -> Callable[[Any], bool]:
        op, val = self.operator, self.value
        if op == "eq":    return lambda v: v == val
        if op == "gt":    return lambda v: v > val
        if op == "lt":    return lambda v: v < val
        if op == "gte":   return lambda v: v >= val
        if op == "lte":   return lambda v: v <= val
        if op == "in":    return lambda v: v in val
        if op == "regex":
            import re
            pattern = re.compile(val)
            return lambda v: bool(pattern.match(str(v)))
        raise ValueError(f"Unknown operator: {op}")

    def evaluate(self, fact: Fact) -> bool:
        if fact.fact_type != self.fact_type:
            return False
        attr_val = fact.get(self.attribute)
        if attr_val is None:
            return False
        return self._test_fn(attr_val)

    def activate(self, fact: Fact) -> None:
        if self.evaluate(fact):
            self.memory.append(fact)
            for child in self.children:
                child.left_activate(fact, self)

#3.2 Alpha Memory Indexing

Without indexing, Beta join is O(N) per activation. A hash index on join attributes brings this to O(1):

Python
from collections import defaultdict

@dataclass
class IndexedAlphaMemory:
    """Hash-indexed alpha memory for O(1) beta joins"""
    index_attribute: str
    _store: dict[Any, list[Fact]] = field(default_factory=lambda: defaultdict(list))
    _all: list[Fact] = field(default_factory=list)

    def add(self, fact: Fact) -> None:
        key = fact.get(self.index_attribute)
        self._store[key].append(fact)
        self._all.append(fact)

    def lookup(self, key: Any) -> list[Fact]:
        return self._store.get(key, [])

    def all(self) -> list[Fact]:
        return self._all

    @property
    def size(self) -> int:
        return len(self._all)

#3.3 Alpha Network Sharing

When multiple rules test the same condition, coomia-dip shares Alpha nodes:

Code
Rule 1: temperature > 80 AND pressure > 100 -> alert("overheat")
Rule 2: temperature > 80 AND humidity < 20  -> alert("fire_risk")

Without sharing:            With sharing:

  [temp>80] [temp>80]          [temp>80]      (shared)
     |         |                /      \
  [pres>100] [hum<20]     [pres>100] [hum<20]
     |         |              |         |
  (Rule1)   (Rule2)       (Rule1)   (Rule2)

Nodes: 4 -> 3  (25% reduction)
Python
class AlphaNetwork:
    """Manages shared alpha nodes with deduplication"""

    def __init__(self):
        self._nodes: dict[str, AlphaNode] = {}

    def _make_key(self, fact_type: FactType, attribute: str,
                  operator: str, value: Any) -> str:
        return f"{fact_type}:{attribute}:{operator}:{value}"

    def get_or_create(self, fact_type: FactType, attribute: str,
                      operator: str, value: Any) -> AlphaNode:
        key = self._make_key(fact_type, attribute, operator, value)
        if key not in self._nodes:
            self._nodes[key] = AlphaNode(
                node_id=key,
                fact_type=fact_type,
                attribute=attribute,
                operator=operator,
                value=value,
            )
        return self._nodes[key]

    def propagate(self, fact: Fact) -> None:
        for node in self._nodes.values():
            node.activate(fact)

    @property
    def shared_ratio(self) -> float:
        """Percentage of nodes saved through sharing"""
        # tracked externally by compiler
        return 0.0

#4. Beta Network: Cross-Fact Join

#4.1 Beta Join Node

Beta nodes join results from two parent memories (Alpha or Beta) based on a join condition:

Code
Beta Join Example:

Alpha Memory 1 (orders):       Alpha Memory 2 (customers):
+------+--------+-------+      +------+--------+------+
| id   | cust   | total |      | id   | region | tier |
+------+--------+-------+      +------+--------+------+
| O001 | C100   | 5000  |      | C100 | APAC   | Gold |
| O002 | C200   | 3000  |      | C200 | EMEA   | Silver|
| O003 | C100   | 8000  |      | C300 | NA     | Gold |
+------+--------+-------+      +------+--------+------+

Join Condition: order.cust == customer.id

Beta Memory (joined tokens):
+---------+---------+--------+-------+--------+------+
| order   | cust    | total  | region| tier   |      |
+---------+---------+--------+-------+--------+------+
| O001    | C100    | 5000   | APAC  | Gold   |      |
| O002    | C200    | 3000   | EMEA  | Silver |      |
| O003    | C100    | 8000   | APAC  | Gold   |      |
+---------+---------+--------+-------+--------+------+
Python
@dataclass
class BetaNode:
    """Joins facts from two parent memories"""
    node_id: str
    left_attr: str       # attribute from left (token binding)
    right_attr: str      # attribute from right (alpha fact)
    operator: str        # usually "eq"
    memory: list[Token] = field(default_factory=list)
    children: list[BetaNode | TerminalNode] = field(default_factory=list)
    _right_index: dict[Any, list[Fact]] = field(
        default_factory=lambda: defaultdict(list)
    )

    def right_activate(self, fact: Fact) -> None:
        """Called when a new fact enters from the alpha (right) side"""
        key = fact.get(self.right_attr)
        self._right_index[key].append(fact)
        # Check existing tokens on left side
        for token in self.memory:
            left_val = token.bindings.get(self.left_attr)
            if left_val == key:
                new_token = token.extend(fact, {self.right_attr: key})
                self._propagate(new_token)

    def left_activate(self, fact: Fact, source: AlphaNode) -> None:
        """Called when a new fact arrives from the alpha (left) side"""
        key = fact.get(self.left_attr)
        token = Token(facts=[fact], bindings={self.left_attr: key})
        # Check existing facts on right side
        for right_fact in self._right_index.get(key, []):
            new_token = token.extend(
                right_fact, {self.right_attr: right_fact.get(self.right_attr)}
            )
            self.memory.append(new_token)
            self._propagate(new_token)

    def _propagate(self, token: Token) -> None:
        for child in self.children:
            if isinstance(child, TerminalNode):
                child.activate(token)
            else:
                child.left_activate_token(token)

#4.2 Negative Conditions (NOT Joins)

coomia-dip supports negated conditions for rules like "alert if NO acknowledgment exists":

Python
@dataclass
class NegativeBetaNode(BetaNode):
    """Fires when NO matching fact exists on the right side"""

    def left_activate(self, fact: Fact, source: AlphaNode) -> None:
        key = fact.get(self.left_attr)
        token = Token(facts=[fact], bindings={self.left_attr: key})
        # Fire only if NO right-side match exists
        if key not in self._right_index or not self._right_index[key]:
            self.memory.append(token)
            self._propagate(token)

    def right_activate(self, fact: Fact) -> None:
        key = fact.get(self.right_attr)
        self._right_index[key].append(fact)
        # Retract tokens that now have a match
        self.memory = [
            t for t in self.memory
            if t.bindings.get(self.left_attr) != key
        ]

#5. Conflict Resolution Strategies

When multiple rules match simultaneously, the engine must decide firing order.

#5.1 Strategy Overview

Code
Conflict Resolution Pipeline:

Matched Rules (Agenda)
        |
        v
+----------------+     +----------------+     +----------------+
| 1. Priority    |---->| 2. Recency     |---->| 3. Specificity |
| (salience)     |     | (newest facts) |     | (most conds)   |
+----------------+     +----------------+     +----------------+
        |                                              |
        v                                              v
  Highest priority                              Most specific
  fires first                                   fires first

#5.2 Implementation

Python
from enum import Enum, auto
from typing import Protocol


class ResolutionStrategy(Protocol):
    def sort_key(self, activation: Activation) -> tuple: ...


@dataclass
class Activation:
    """A rule matched with specific facts, ready to fire"""
    rule: Rule
    token: Token
    timestamp: float = field(default_factory=time.time)

    @property
    def recency(self) -> float:
        return max(f.timestamp for f in self.token.facts)

    @property
    def specificity(self) -> int:
        return len(self.token.facts)


class PriorityStrategy:
    def sort_key(self, a: Activation) -> tuple:
        return (-a.rule.priority,)


class RecencyStrategy:
    def sort_key(self, a: Activation) -> tuple:
        return (-a.recency,)


class SpecificityStrategy:
    def sort_key(self, a: Activation) -> tuple:
        return (-a.specificity,)


class CompositeStrategy:
    """Combines multiple strategies with configurable weights"""
    def __init__(self, strategies: list[ResolutionStrategy]):
        self._strategies = strategies

    def sort_key(self, a: Activation) -> tuple:
        keys = ()
        for s in self._strategies:
            keys += s.sort_key(a)
        return keys


class Agenda:
    """Priority queue of activations awaiting execution"""
    def __init__(self, strategy: ResolutionStrategy | None = None):
        self._activations: list[Activation] = []
        self._strategy = strategy or CompositeStrategy([
            PriorityStrategy(),
            RecencyStrategy(),
            SpecificityStrategy(),
        ])

    def add(self, activation: Activation) -> None:
        self._activations.append(activation)

    def pop_next(self) -> Activation | None:
        if not self._activations:
            return None
        self._activations.sort(key=self._strategy.sort_key)
        return self._activations.pop(0)

    @property
    def size(self) -> int:
        return len(self._activations)

#5.3 Strategy Comparison

StrategyBest ForDrawback
PriorityBusiness-critical rules firstManual assignment needed
RecencyReact to latest dataMay starve older activations
SpecificityMost precise match winsComplex rules always win
CompositeEnterprise production useSlightly more computation

#6. Incremental Fact Updates

#6.1 Why Incremental Matters

In production, facts change continuously. Recomputing the entire Rete network on every change is wasteful. coomia-dip supports three incremental operations:

Code
Incremental Operations:

ASSERT (add fact)    RETRACT (remove fact)    MODIFY (update fact)
      |                     |                      |
      v                     v                      v
  Propagate           Remove from            RETRACT old +
  through             all memories            ASSERT new
  alpha net            + retract tokens

#6.2 Implementation

Python
class WorkingMemory:
    """Manages facts with incremental change propagation"""

    def __init__(self, rete_network: ReteNetwork):
        self._facts: dict[str, Fact] = {}
        self._rete = rete_network

    def assert_fact(self, fact: Fact) -> None:
        if fact.fact_id in self._facts:
            raise ValueError(f"Fact {fact.fact_id} already exists. Use modify().")
        self._facts[fact.fact_id] = fact
        self._rete.propagate_assert(fact)

    def retract_fact(self, fact_id: str) -> None:
        fact = self._facts.pop(fact_id, None)
        if fact is None:
            return
        self._rete.propagate_retract(fact)

    def modify_fact(self, fact_id: str, updates: dict[str, Any]) -> None:
        old = self._facts.get(fact_id)
        if old is None:
            raise KeyError(f"Fact {fact_id} not found")
        # Retract old, assert new (standard Rete approach)
        self.retract_fact(fact_id)
        new_attrs = dict(old.attributes)
        new_attrs.update(updates)
        new_fact = Fact(
            fact_id=fact_id,
            fact_type=old.fact_type,
            attributes=tuple(new_attrs.items()),
        )
        self.assert_fact(new_fact)

    @property
    def fact_count(self) -> int:
        return len(self._facts)

#7. Complete Engine: Putting It All Together

#7.1 ReteNetwork Class

Python
@dataclass
class Rule:
    """A business rule with conditions and an action"""
    rule_id: str
    name: str
    priority: int = 0
    conditions: list[dict] = field(default_factory=list)
    action: Callable[[Token], Fact | None] = field(default=lambda t: None)


@dataclass
class TerminalNode:
    """Represents a fully matched rule"""
    rule: Rule
    agenda: Agenda

    def activate(self, token: Token) -> None:
        self.agenda.add(Activation(rule=self.rule, token=token))


class ReteNetwork:
    """Complete Rete network for forward-chain reasoning"""

    def __init__(self):
        self.alpha_network = AlphaNetwork()
        self.beta_nodes: list[BetaNode] = []
        self.terminal_nodes: list[TerminalNode] = []
        self.agenda = Agenda()
        self._compiled = False

    def add_rule(self, rule: Rule) -> None:
        """Compile a rule into alpha/beta/terminal nodes"""
        alpha_nodes = []
        for cond in rule.conditions:
            alpha = self.alpha_network.get_or_create(
                fact_type=FactType(cond["fact_type"]),
                attribute=cond["attribute"],
                operator=cond["operator"],
                value=cond["value"],
            )
            alpha_nodes.append(alpha)

        # Build beta chain for multi-condition rules
        if len(alpha_nodes) == 1:
            terminal = TerminalNode(rule=rule, agenda=self.agenda)
            alpha_nodes[0].children.append(terminal)
            self.terminal_nodes.append(terminal)
        else:
            prev_beta = None
            for i in range(1, len(alpha_nodes)):
                join_cond = rule.conditions[i].get("join", {})
                beta = BetaNode(
                    node_id=f"beta_{rule.rule_id}_{i}",
                    left_attr=join_cond.get("left", "id"),
                    right_attr=join_cond.get("right", "id"),
                    operator="eq",
                )
                if prev_beta:
                    prev_beta.children.append(beta)
                else:
                    alpha_nodes[0].children.append(beta)
                alpha_nodes[i].children.append(beta)
                self.beta_nodes.append(beta)
                prev_beta = beta

            terminal = TerminalNode(rule=rule, agenda=self.agenda)
            prev_beta.children.append(terminal)
            self.terminal_nodes.append(terminal)

        self._compiled = True

    def propagate_assert(self, fact: Fact) -> None:
        self.alpha_network.propagate(fact)

    def propagate_retract(self, fact: Fact) -> None:
        # Remove from all alpha memories
        for node in self.alpha_network._nodes.values():
            node.memory = [f for f in node.memory if f.fact_id != fact.fact_id]
        # Remove from beta memories
        for beta in self.beta_nodes:
            beta.memory = [
                t for t in beta.memory
                if all(f.fact_id != fact.fact_id for f in t.facts)
            ]

    def run_to_completion(self, working_memory: WorkingMemory,
                          max_cycles: int = 1000) -> list[Fact]:
        """Execute the recognize-act cycle until quiescence"""
        derived: list[Fact] = []
        cycle = 0
        while cycle < max_cycles:
            activation = self.agenda.pop_next()
            if activation is None:
                break
            result = activation.rule.action(activation.token)
            if result and result.fact_id not in {f.fact_id for f in derived}:
                derived.append(result)
                working_memory.assert_fact(result)
            cycle += 1
        return derived

#7.2 Usage Example

Python
# Define rules for supply chain risk assessment
risk_rules = [
    Rule(
        rule_id="R001",
        name="low_inventory_alert",
        priority=10,
        conditions=[
            {
                "fact_type": "metric",
                "attribute": "inventory_level",
                "operator": "lt",
                "value": 100,
            }
        ],
        action=lambda t: Fact(
            fact_id=f"alert_{t.facts[0].fact_id}",
            fact_type=FactType.EVENT,
            attributes=(("type", "low_inventory"), ("severity", "high")),
        ),
    ),
    Rule(
        rule_id="R002",
        name="critical_supplier_risk",
        priority=20,
        conditions=[
            {
                "fact_type": "event",
                "attribute": "type",
                "operator": "eq",
                "value": "low_inventory",
            },
            {
                "fact_type": "object",
                "attribute": "supplier_tier",
                "operator": "eq",
                "value": "critical",
                "join": {"left": "severity", "right": "risk_level"},
            },
        ],
        action=lambda t: Fact(
            fact_id=f"escalation_{t.facts[0].fact_id}",
            fact_type=FactType.EVENT,
            attributes=(("type", "supply_chain_escalation"), ("priority", "P1")),
        ),
    ),
]

# Build and run
network = ReteNetwork()
for rule in risk_rules:
    network.add_rule(rule)

wm = WorkingMemory(network)
wm.assert_fact(Fact(
    fact_id="inv_001",
    fact_type=FactType.METRIC,
    attributes=(("inventory_level", 42), ("warehouse", "SH-01")),
))

results = network.run_to_completion(wm)
print(f"Derived {len(results)} new facts")

#8. Performance Benchmarks

#8.1 Test Setup

ParameterValue
Rules1,000 / 5,000 / 10,000
Facts10,000 / 50,000 / 100,000
Conditions per rule2-5
Machine8 vCPU, 32 GB RAM
Python3.12 with Cython hot paths

#8.2 Latency Results

Code
Assert latency (ms) per fact, by rule count:

         1K rules    5K rules    10K rules
         --------    --------    ---------
10K  F   |  0.8   |    2.1   |     4.5   |
50K  F   |  1.2   |    3.8   |     8.2   |
100K F   |  1.8   |    5.5   |    12.1   |

Run-to-completion (ms), 10K facts, by rule count:

         1K rules    5K rules    10K rules
         --------    --------    ---------
         |  45    |    120   |     195   |

#8.3 Memory Usage

Code
Memory consumption (MB):

                 1K rules    5K rules    10K rules
                 --------    --------    ---------
Alpha nodes      |  12    |    55    |     108   |
Beta nodes       |   8    |    42    |      85   |
Fact storage     |  25    |    25    |      25   |  (10K facts)
Indexes          |   5    |    18    |      35   |
                 --------    --------    ---------
Total            |  50    |   140    |     253   |

#8.4 Node Sharing Impact

MetricWithout SharingWith SharingImprovement
Alpha nodes4,2002,52040% fewer
Memory (MB)38025333% less
Assert latency18ms12ms33% faster

#9. gRPC Integration with Reasoning & Decision Layer

#9.1 Service Definition

PROTOBUF
// reasoning_service.proto
syntax = "proto3";
package onto.reasoning.v1;

service ReasoningService {
    // Assert a fact into working memory
    rpc AssertFact(AssertFactRequest) returns (AssertFactResponse);

    // Run forward-chain reasoning to completion
    rpc RunReasoning(RunReasoningRequest) returns (stream ReasoningEvent);

    // Query current agenda state
    rpc GetAgenda(GetAgendaRequest) returns (AgendaSnapshot);
}

message AssertFactRequest {
    string fact_id = 1;
    string fact_type = 2;
    map<string, string> attributes = 3;
}

message ReasoningEvent {
    string event_type = 1;   // "rule_fired", "fact_derived", "cycle_complete"
    string rule_id = 2;
    string fact_id = 3;
    int32 cycle = 4;
    double latency_ms = 5;
}

#9.2 gRPC Server Implementation

Python
import grpc
from concurrent import futures
from onto.reasoning.v1 import reasoning_service_pb2_grpc as pb2_grpc
from onto.reasoning.v1 import reasoning_service_pb2 as pb2


class ReasoningServicer(pb2_grpc.ReasoningServiceServicer):
    """gRPC server wrapping the Rete engine"""

    def __init__(self, network: ReteNetwork, wm: WorkingMemory):
        self._network = network
        self._wm = wm

    async def AssertFact(self, request, context):
        fact = Fact(
            fact_id=request.fact_id,
            fact_type=FactType(request.fact_type),
            attributes=tuple(request.attributes.items()),
        )
        self._wm.assert_fact(fact)
        return pb2.AssertFactResponse(
            success=True,
            agenda_size=self._network.agenda.size,
        )

    async def RunReasoning(self, request, context):
        """Streams reasoning events as rules fire"""
        cycle = 0
        while cycle < request.max_cycles:
            activation = self._network.agenda.pop_next()
            if activation is None:
                break
            start = time.monotonic()
            result = activation.rule.action(activation.token)
            latency = (time.monotonic() - start) * 1000

            yield pb2.ReasoningEvent(
                event_type="rule_fired",
                rule_id=activation.rule.rule_id,
                fact_id=result.fact_id if result else "",
                cycle=cycle,
                latency_ms=latency,
            )

            if result:
                self._wm.assert_fact(result)

            cycle += 1


def serve():
    server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
    network = ReteNetwork()
    wm = WorkingMemory(network)
    # ... load rules ...
    pb2_grpc.add_ReasoningServiceServicer_to_server(
        ReasoningServicer(network, wm), server
    )
    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()

#10. Debugging and Tracing

#10.1 Activation Trace

Every rule firing is recorded for auditability:

Python
@dataclass
class ActivationTrace:
    """Records the full path of a rule activation for debugging"""
    rule_id: str
    rule_name: str
    matched_facts: list[str]         # fact IDs
    alpha_nodes_hit: list[str]       # alpha node IDs
    beta_joins: list[str]            # beta node IDs
    cycle: int
    timestamp: float
    latency_ms: float

    def to_ascii(self) -> str:
        lines = [f"=== Activation Trace: {self.rule_name} ==="]
        lines.append(f"Cycle {self.cycle} at {self.timestamp:.3f}")
        lines.append(f"Latency: {self.latency_ms:.2f}ms")
        lines.append("Path:")
        for alpha in self.alpha_nodes_hit:
            lines.append(f"  PASS  Alpha({alpha})")
        for beta in self.beta_joins:
            lines.append(f"  JOIN  Beta({beta})")
        lines.append(f"  FIRE  Terminal({self.rule_id})")
        lines.append(f"Facts: {', '.join(self.matched_facts)}")
        return "\n".join(lines)

#10.2 Visualization

Code
Trace: supply_chain_escalation (Cycle 3)

  Fact: inv_001 (metric, inventory_level=42)
       |
       v
  [Alpha: metric.inventory_level < 100] --> PASS
       |
       v
  [Alpha Memory: 1 fact stored]
       |
       v
  [Terminal: R001 low_inventory_alert] --> FIRE
       |
       v
  Derived: alert_inv_001 (event, type=low_inventory)
       |
       v
  [Alpha: event.type == "low_inventory"] --> PASS
       |               |
       |          [Beta Join: severity == risk_level]
       |               |
       v               v
  [Terminal: R002 critical_supplier_risk] --> FIRE
       |
       v
  Derived: escalation_alert_inv_001 (event, type=supply_chain_escalation)

#Key Takeaways

  1. Rete algorithm compiles rule conditions into a shared matching network, achieving efficient reasoning through space-time tradeoff
  2. Alpha network handles single-fact conditions; Beta network handles cross-fact join conditions
  3. Alpha Memory indexing optimizes join matching from O(N) to O(1)
  4. Conflict resolution supports priority, recency, specificity, and composite strategies
  5. Node sharing reduces 40-60% of network nodes, avoiding redundant computation
  6. With 10K rules + 100K facts, Rete network latency remains under 200ms
  7. Integration with Reasoning & Decision Layer via gRPC enables streaming reasoning events

#Next Article

Next up: S5-03 Hybrid Reasoning: When Rule Engines Meet Machine Learning will show how to combine deterministic rule reasoning with probabilistic ML inference, building a multi-layer hybrid reasoning strategy.

tags: #rule-engine #forward-chain #rete-network #alpha-node #beta-node #pattern-matching #coomia-dip