Skip to main content

Agent-as-Grid-Node

Core Concepts

Agent-as-Grid-Node

Agent-as-Grid-Node is a core design philosophy of EnerOS: every Agent is logically a node in the power grid, with a well-defined electrical position, jurisdiction, permission boundary, and lifecycle. An Agent is not a program floating outside the grid; it is a first-class participant in the grid topology and naturally collaborates with other nodes through the grid topology.

Design Philosophy

Limitations of Traditional AgentOS

General-purpose AgentOS (such as AutoGen, LangGraph, CrewAI) treats Agents as pure logical units decoupled from the physical world. This model faces serious problems in power scenarios:

ProblemDescriptionConsequence
No location awarenessAgent does not know which substation it managesDecisions cannot be executed
No topology awarenessAgents communicate through predefined channelsCannot handle topology changes
No permission boundaryAgent can operate any device arbitrarilyHigh security risk
No physical constraintsAgent decisions may violate electrical lawsDecisions are infeasible
No state synchronizationAgent state is decoupled from grid stateDecisions based on stale data

Agent-as-Grid-Node Solution

EnerOS binds each Agent to a specific node in the grid, endowing it with:

  1. Position: Explicitly bound to a bus, substation, or device
  2. Connection: Communicates with other Agents through grid topology
  3. State: Perceives and maintains the operational state of the node it is on
  4. Behavior: Can perform control on its node within permission scope
  5. Boundary: Physical and logical permission boundaries enforced by the kernel

Agent Node Model

Node Attributes

Each Agent node contains the following kernel-maintained attributes:

AttributeTypeDescriptionExample
node_idNodeIdBound electrical node IDBusId(1) / DeviceId(“tr-001”)
node_typeNodeTypeNode typeBus / Device / Switch / Substation
electrical_positionElectricalPosElectrical position (voltage level/region)110kV / East Region
jurisdictionJurisdictionJurisdiction (device set)[Breaker-1, Breaker-2, Tr-1]
permissionsPermissionSetPermission bitmaskREAD | WRITE | CONTROL
priorityPriorityScheduling priorityNormal / High / Critical
livenessLivenessLifecycle stateInit / Active / Paused / Dead
use eneros_agent::{Agent, AgentContext, AgentResult, NodeBinding, Permission};
use eneros_topology::{BusId, VoltageLevel, RegionId};

/// Agent node binding information (kernel-maintained)
#[derive(Clone, Debug)]
pub struct NodeBinding {
    /// Bound electrical node ID
    pub node_id: BusId,
    /// Node type
    pub node_type: NodeType,
    /// Electrical position
    pub electrical_position: ElectricalPosition,
    /// Jurisdictional device set
    pub jurisdiction: Vec<DeviceId>,
    /// Permission bitmask
    pub permissions: PermissionSet,
    /// Scheduling priority
    pub priority: Priority,
}

/// Electrical position
#[derive(Clone, Debug)]
pub struct ElectricalPosition {
    pub voltage_level: VoltageLevel, // 500kV / 220kV / 110kV / 35kV / 10kV
    pub region: RegionId,             // Dispatch region
    pub substation: Option<SubstationId>,
}

/// Permission bits
bitflags::bitflags! {
    pub struct PermissionSet: u32 {
        const READ       = 0b0000_0001; // Read node state
        const WRITE      = 0b0000_0010; // Write node attributes
        const CONTROL    = 0b0000_0100; // Control devices (switches, voltage regulation)
        const DISPATCH   = 0b0000_1000; // Dispatch generation output
        const TRADE      = 0b0001_0000; // Participate in electricity trading
        const ISLAND     = 0b0010_0000; // Islanding operation authorization
        const EMERGENCY  = 0b0100_0000; // Emergency control permission
    }
}

Node Binding Example

use eneros_agent::{AgentRegistry, NodeBinding, PermissionSet, Priority};

let mut registry = AgentRegistry::new();

// Create a dispatch Agent bound to bus 1
let binding = NodeBinding {
    node_id: BusId(1),
    node_type: NodeType::Bus,
    electrical_position: ElectricalPosition {
        voltage_level: VoltageLevel::Kv110,
        region: RegionId::from("east"),
        substation: Some(SubstationId::from("ss-001")),
    },
    jurisdiction: vec![
        DeviceId::from("gen-1"),
        DeviceId::from("gen-2"),
        DeviceId::from("tr-1"),
    ],
    permissions: PermissionSet::READ | PermissionSet::DISPATCH | PermissionSet::CONTROL,
    priority: Priority::High,
};

let agent = DispatchAgent::new(binding.clone());
registry.register("dispatch-east-1", agent, binding).await?;

Agent Types and Node Binding

Different types of Agents bind to different types of grid nodes:

Agent TypeNode TypeTypical BindingJurisdictionPermissions
DispatchAgentGeneration busPower plant/Wind farmGenerator unitsREAD+DISPATCH
SelfHealingAgentSwitch/SectionalizerDistribution switchFeeder segmentsREAD+CONTROL+EMERGENCY
OperationAgentDeviceTransformer/BreakerSingle deviceREAD+WRITE+CONTROL
EnergyAgentLoad busIndustrial park/Commercial areaLoad aggregationREAD+TRADE
TradingAgentRegional root nodeVirtual power plantRegional resourcesREAD+TRADE+DISPATCH
ProtectionAgentProtection deviceSubstation protectionProtection zoneREAD+EMERGENCY
MonitoringAgentAny nodeMonitoring pointMeasurement point setREAD

Node Lifecycle

Agent nodes follow a well-defined lifecycle managed by the kernel:

   ┌─────────┐  register   ┌─────────┐  activate   ┌─────────┐
   │  Init   │ ──────────→ │ Pending │ ──────────→ │ Active  │
   └─────────┘             └─────────┘             └─────────┘

                                  ┌──────────────────────┤
                                  │ pause                │ error
                                  ▼                      ▼
                              ┌─────────┐           ┌─────────┐
                              │ Paused  │           │ Degraded│
                              └─────────┘           └─────────┘
                                  │                      │
                                  │ resume               │ recover
                                  └──────┬───────────────┘

                                     ┌─────────┐
                                     │ Active  │
                                     └─────────┘

                                         │ shutdown

                                     ┌─────────┐
                                     │  Dead   │
                                     └─────────┘
StateDescriptionKernel Behavior
InitCreated but not registeredNo resources allocated
PendingRegistered but not activatedResources allocated, not scheduled
ActiveRunningNormal scheduling, participates in collaboration
PausedSuspendedState preserved, not scheduled
DegradedDegraded operationRead-only permissions only
DeadTerminatedResources released, node deregistered
use eneros_agent::{LifecycleEvent, LifecycleManager};

let lifecycle = LifecycleManager::new(&registry);

// Register and activate
lifecycle.register("dispatch-east-1", agent).await?;
lifecycle.activate("dispatch-east-1").await?;

// Listen to lifecycle events
while let Some(event) = lifecycle.events().recv().await {
    match event {
        LifecycleEvent::Activated { agent_id, node_id } => {
            log::info!("Agent {} activated on node {}", agent_id, node_id);
        }
        LifecycleEvent::Paused { agent_id, reason } => {
            log::warn!("Agent {} paused: {}", agent_id, reason);
        }
        LifecycleEvent::Degraded { agent_id, error } => {
            log::error!("Agent {} degraded: {:?}", agent_id, error);
        }
        LifecycleEvent::Terminated { agent_id } => {
            log::info!("Agent {} terminated", agent_id);
        }
    }
}

Inter-Node Communication

Topology-Aware Communication

Communication between Agents is not through predefined IP/ports, but automatically routed through the grid topology:

impl Agent for DispatchAgent {
    async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
        // Get neighbor nodes in the same electrical island
        let neighbors = ctx.get_neighbors(self.node_id).await?;

        // Send messages to downstream Agents via topology path
        for neighbor in neighbors {
            if neighbor.is_load_bus() {
                ctx.send_to_node(
                    neighbor.node_id,
                    Message::LoadShed { amount_mw: 5.0 },
                ).await?;
            }
        }

        // Broadcast to device Agents within jurisdiction
        ctx.broadcast_to_jurisdiction(
            self.binding.jurisdiction.clone(),
            Message::SetGeneration { target_mw: 80.0 },
        ).await?;

        Ok(())
    }
}

Communication Patterns

EnerOS supports four inter-node communication patterns:

PatternSemanticsLatencyApplicable Scenarios
send_to_nodePoint-to-point, single recipient< 100μsDirect collaboration
broadcast_to_jurisdictionBroadcast to jurisdictional devices< 500μsDevice synchronous control
publish_to_islandPublish to all nodes in electrical island< 1msIsland-level broadcast
request_responseRequest-response, with timeout< 5msNegotiated collaboration
// 1. Point-to-point
let ack = ctx.send_to_node(
    BusId(2),
    Message::VoltageAdjust { target_pu: 1.0 },
).await?;

// 2. Broadcast to jurisdiction
ctx.broadcast_to_jurisdiction(
    self.jurisdiction.clone(),
    Message::SyncState { timestamp: now!() },
).await?;

// 3. Electrical island publish
ctx.publish_to_island(
    self.node_id,
    Topic::FrequencyEvent { hz: 49.8 },
).await?;

// 4. Request-response
let response = ctx.request(
    BusId(3),
    Request::GetState,
    Duration::milliseconds(100),
).await?;

Topology-Aware Collaboration

Single-Line Collaboration Example

Bus1 ─── Line ─── Bus2 ─── Transformer ─── Bus3
  |                    |                    |
DispatchAgent A    OperationAgent B    EnergyAgent C

When DispatchAgent A adjusts generation output:

  1. Agent A becomes aware through topology that Bus 2 and 3 are downstream nodes
  2. Agent A publishes a GenerationChange event to the electrical island
  3. Agent B (transformer operations) perceives the power flow change and adjusts the tap
  4. Agent C (load aggregation) perceives the voltage change and adjusts load distribution
impl Agent for DispatchAgent {
    async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
        let target_mw = 80.0;

        // 1. Verify own permissions
        ctx.check_permission(Permission::DISPATCH)?;

        // 2. Get downstream nodes via topology
        let downstream = ctx.get_downstream_nodes(self.node_id).await?;

        // 3. Publish generation change event (topology-aware broadcast)
        ctx.publish_to_island(
            self.node_id,
            Event::GenerationChange {
                bus: self.node_id,
                from_mw: self.current_output,
                to_mw: target_mw,
                timestamp: now!(),
            },
        ).await?;

        // 4. Execute generation output adjustment
        ctx.set_node_generation(self.node_id, target_mw).await?;

        // 5. Wait for downstream Agent responses (with timeout)
        let acks = ctx.wait_for_acks(
            downstream.iter().map(|n| n.node_id).collect(),
            Duration::seconds(1),
        ).await?;

        if acks.failed_count() > 0 {
            log::warn!("Some downstream nodes did not acknowledge: {:?}", acks.failures());
        }

        Ok(())
    }
}

Multi-Agent Fault Self-Healing Collaboration

     [F] Fault point

BusA ─── Breaker1 ─── BusB ─── Breaker2 ─── BusC
  |                              |                    |
SelfHealingAgent X           SelfHealingAgent Y   EnergyAgent Z

Fault self-healing process:

impl Agent for SelfHealingAgent {
    async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
        // Listen for fault events
        let fault = ctx.wait_event::<FaultEvent>().await?;

        // 1. Locate the fault section
        let fault_section = ctx.locate_fault(&fault).await?;

        // 2. Isolate the fault: operate breakers within jurisdiction
        for breaker in self.jurisdiction.breakers_around(fault_section) {
            ctx.open_breaker(breaker).await?;
        }

        // 3. Identify non-faulted outaged areas through topology
        let outaged = ctx.find_outaged_areas().await?;

        // 4. Negotiate with upstream Agents to restore power
        for area in outaged {
            let upstream = ctx.find_upstream_sources(area).await?;
            for source_node in upstream {
                let restored = ctx.request(
                    source_node,
                    Request::RestoreSupply { area, load_mw: area.load_mw },
                    Duration::seconds(2),
                ).await?;

                if restored.is_ok() {
                    ctx.close_breaker(area.tie_breaker).await?;
                    break;
                }
            }
        }

        Ok(())
    }
}

Comparison with General-Purpose AgentOS

DimensionGeneral AgentOSEnerOS Agent-as-Grid-Node
Agent identityLogical IDGrid node ID
Communication methodPredefined channels/IPTopology-aware routing
Location awarenessNoneBound to electrical node
Permission boundaryApplication-layer validationKernel enforcement
State sourceSelf-maintainedKernel synchronization
Collaboration triggerExplicit invocationTopology event-driven
Fault perceptionExternal notificationTopology change is an event
Decision feasibilityRelies on application validationKernel constraint guarantee
Horizontal scalingManual configurationTopology auto-discovery
Consistency modelEventual consistencyTopology strong consistency

Permission Boundary

Three-Layer Permission Model

EnerOS ensures Agent operation safety through a three-layer permission model:

┌─────────────────────────────────────────┐
│ 1. Node Permission                       │  ← Declared at Agent binding
│   · READ / WRITE / CONTROL / DISPATCH   │
├─────────────────────────────────────────┤
│ 2. Jurisdiction                          │  ← Kernel-maintained device set
│   · Can only operate devices within      │
│     jurisdiction                          │
├─────────────────────────────────────────┤
│ 3. Constraint                            │  ← Physical constraint enforcement
│   · Even with permission, operations     │
│     that violate constraints are rejected│
└─────────────────────────────────────────┘
// Permission validation example
impl AgentContext {
    pub async fn open_breaker(&mut self, breaker_id: DeviceId) -> AgentResult<()> {
        // 1. Node permission check
        if !self.binding.permissions.contains(Permission::CONTROL) {
            return Err(AgentError::PermissionDenied("Missing CONTROL permission"));
        }

        // 2. Jurisdiction check
        if !self.binding.jurisdiction.contains(&breaker_id) {
            return Err(AgentError::OutOfJurisdiction(breaker_id));
        }

        // 3. Constraint check (kernel automatic)
        let command = Command::OpenBreaker(breaker_id);
        match self.syscall(ExecuteCommand(command)).await? {
            ExecuteResult::Executed => Ok(()),
            ExecuteResult::Projected(_) => Err(AgentError::CommandProjected),
            ExecuteResult::Rejected(r) => Err(AgentError::ConstraintRejected(r)),
        }
    }
}

Node Discovery and Auto-Registration

EnerOS supports topology-based Agent auto-discovery and registration:

use eneros_agent::{AutoDiscovery, AgentFactory};

let discovery = AutoDiscovery::new(&network, &mut registry);

// Listen for topology changes and auto-deploy Agents
discovery.on_new_bus(move |bus_id, bus_info| {
    if bus_info.has_generator() {
        // New generation bus -> deploy DispatchAgent
        let agent = DispatchAgent::new(NodeBinding::for_bus(bus_id));
        Some(Box::new(agent))
    } else if bus_info.has_load() {
        // New load bus -> deploy EnergyAgent
        let agent = EnergyAgent::new(NodeBinding::for_bus(bus_id));
        Some(Box::new(agent))
    } else {
        None
    }
}).await?;

// Topology changes trigger Agent auto-registration/deregistration
discovery.start().await?;

Performance Metrics

OperationLatencyThroughput
Node binding< 1ms-
Topology-aware neighbor query< 10μs1 million/sec
Inter-node message passing< 100μs100k messages/sec
Jurisdiction broadcast< 500μs-
Electrical island publish< 1ms-
Permission check< 1μs10 million/sec
Agent context switch< 10μs-

Limitations and Trade-offs

Trade-offDescriptionMitigation Strategy
Node couplingAgent is tightly coupled with node, migration is difficultSupports rebinding API
Single-point responsibilityOne node can only bind one master AgentSupports backup Agent hot standby
Topology dependencyTopology changes affect Agent lifecycleAuto-discovery mechanism for smooth transitions
Debugging complexityCross-node collaboration chains are hard to traceBuilt-in distributed tracing

Next Steps