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:
| Problem | Description | Consequence |
|---|---|---|
| No location awareness | Agent does not know which substation it manages | Decisions cannot be executed |
| No topology awareness | Agents communicate through predefined channels | Cannot handle topology changes |
| No permission boundary | Agent can operate any device arbitrarily | High security risk |
| No physical constraints | Agent decisions may violate electrical laws | Decisions are infeasible |
| No state synchronization | Agent state is decoupled from grid state | Decisions based on stale data |
Agent-as-Grid-Node Solution
EnerOS binds each Agent to a specific node in the grid, endowing it with:
- Position: Explicitly bound to a bus, substation, or device
- Connection: Communicates with other Agents through grid topology
- State: Perceives and maintains the operational state of the node it is on
- Behavior: Can perform control on its node within permission scope
- Boundary: Physical and logical permission boundaries enforced by the kernel
Agent Node Model
Node Attributes
Each Agent node contains the following kernel-maintained attributes:
| Attribute | Type | Description | Example |
|---|---|---|---|
node_id | NodeId | Bound electrical node ID | BusId(1) / DeviceId(“tr-001”) |
node_type | NodeType | Node type | Bus / Device / Switch / Substation |
electrical_position | ElectricalPos | Electrical position (voltage level/region) | 110kV / East Region |
jurisdiction | Jurisdiction | Jurisdiction (device set) | [Breaker-1, Breaker-2, Tr-1] |
permissions | PermissionSet | Permission bitmask | READ | WRITE | CONTROL |
priority | Priority | Scheduling priority | Normal / High / Critical |
liveness | Liveness | Lifecycle state | Init / 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 Type | Node Type | Typical Binding | Jurisdiction | Permissions |
|---|---|---|---|---|
| DispatchAgent | Generation bus | Power plant/Wind farm | Generator units | READ+DISPATCH |
| SelfHealingAgent | Switch/Sectionalizer | Distribution switch | Feeder segments | READ+CONTROL+EMERGENCY |
| OperationAgent | Device | Transformer/Breaker | Single device | READ+WRITE+CONTROL |
| EnergyAgent | Load bus | Industrial park/Commercial area | Load aggregation | READ+TRADE |
| TradingAgent | Regional root node | Virtual power plant | Regional resources | READ+TRADE+DISPATCH |
| ProtectionAgent | Protection device | Substation protection | Protection zone | READ+EMERGENCY |
| MonitoringAgent | Any node | Monitoring point | Measurement point set | READ |
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 │
└─────────┘
| State | Description | Kernel Behavior |
|---|---|---|
| Init | Created but not registered | No resources allocated |
| Pending | Registered but not activated | Resources allocated, not scheduled |
| Active | Running | Normal scheduling, participates in collaboration |
| Paused | Suspended | State preserved, not scheduled |
| Degraded | Degraded operation | Read-only permissions only |
| Dead | Terminated | Resources released, node deregistered |
use eneros_agent::{LifecycleEvent, LifecycleManager};
let lifecycle = LifecycleManager::new(®istry);
// 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:
| Pattern | Semantics | Latency | Applicable Scenarios |
|---|---|---|---|
send_to_node | Point-to-point, single recipient | < 100μs | Direct collaboration |
broadcast_to_jurisdiction | Broadcast to jurisdictional devices | < 500μs | Device synchronous control |
publish_to_island | Publish to all nodes in electrical island | < 1ms | Island-level broadcast |
request_response | Request-response, with timeout | < 5ms | Negotiated 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:
- Agent A becomes aware through topology that Bus 2 and 3 are downstream nodes
- Agent A publishes a
GenerationChangeevent to the electrical island - Agent B (transformer operations) perceives the power flow change and adjusts the tap
- 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
| Dimension | General AgentOS | EnerOS Agent-as-Grid-Node |
|---|---|---|
| Agent identity | Logical ID | Grid node ID |
| Communication method | Predefined channels/IP | Topology-aware routing |
| Location awareness | None | Bound to electrical node |
| Permission boundary | Application-layer validation | Kernel enforcement |
| State source | Self-maintained | Kernel synchronization |
| Collaboration trigger | Explicit invocation | Topology event-driven |
| Fault perception | External notification | Topology change is an event |
| Decision feasibility | Relies on application validation | Kernel constraint guarantee |
| Horizontal scaling | Manual configuration | Topology auto-discovery |
| Consistency model | Eventual consistency | Topology 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
| Operation | Latency | Throughput |
|---|---|---|
| Node binding | < 1ms | - |
| Topology-aware neighbor query | < 10μs | 1 million/sec |
| Inter-node message passing | < 100μs | 100k messages/sec |
| Jurisdiction broadcast | < 500μs | - |
| Electrical island publish | < 1ms | - |
| Permission check | < 1μs | 10 million/sec |
| Agent context switch | < 10μs | - |
Limitations and Trade-offs
| Trade-off | Description | Mitigation Strategy |
|---|---|---|
| Node coupling | Agent is tightly coupled with node, migration is difficult | Supports rebinding API |
| Single-point responsibility | One node can only bind one master Agent | Supports backup Agent hot standby |
| Topology dependency | Topology changes affect Agent lifecycle | Auto-discovery mechanism for smooth transitions |
| Debugging complexity | Cross-node collaboration chains are hard to trace | Built-in distributed tracing |
Next Steps
- Power-Native First - Power-Native First design philosophy
- Constraint as Law - Underlying guarantee of permission boundaries
- Multi-Agent Collaboration - Agent collaboration mechanism details
- Real-Time Determinism - Real-time guarantees for node scheduling