Skip to main content

eneros-agent

Crate Index

eneros-agent

eneros-agent provides the Agent runtime and built-in domain Agents, treating Agents as first-class grid nodes within the operating system’s scheduling. Agents directly access topology, Load Flow, time series, and devices through kernel APIs without redundant modeling. EnerOS includes four categories of built-in domain Agents (DispatchAgent / SelfHealingAgent / TradingAgent / PlanningAgent), and provides the Agent trait for user customization.

Responsibilities

eneros-agent handles the following core responsibilities:

  • Agent Lifecycle: Register → Start → Pause → Resume → Stop, state machine management
  • Domain Agent Implementation: Four built-in Agent categories: dispatch, self-healing, trading, planning
  • Multi-Agent Collaboration: Communication through the event bus (eneros-eventbus), with Orchestrator orchestrating task decomposition and aggregation
  • Tool Invocation: Agents execute kernel APIs (Load Flow, constraints, control) through the tool engine provided by eneros-tool
  • Memory and Reasoning: Integrates eneros-memory short-term/long-term memory and eneros-reasoning reasoning engine (LLM)
  • Grid Location Awareness: Each Agent has a clear electrical location and jurisdiction within the topology
  • Permission Boundaries: Role and jurisdiction-based access control; unauthorized operations are rejected by the gateway

Key Types and Interfaces

Agent trait

use async_trait::async_trait;
use eneros_core::{AgentId, Result};
use eneros_topology::NetworkGraph;
use eneros_timeseries::TimeSeriesEngine;
use eneros_eventbus::EventBus;
use eneros_tool::ToolRegistry;

#[async_trait]
pub trait Agent: Send + Sync {
    fn id(&self) -> &AgentId;
    fn agent_type(&self) -> AgentType;
    fn jurisdiction(&self) -> &Jurisdiction;
    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> AgentResult<()>;
    async fn on_event(&mut self, event: &Event) -> AgentResult<()>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentType {
    Dispatch,
    SelfHealing,
    Trading,
    Planning,
    Custom,
}

#[derive(Debug, Clone)]
pub struct Jurisdiction {
    pub buses: Vec<eneros_core::BusId>,
    pub branches: Vec<eneros_core::BranchId>,
    pub region: Option<String>,
    pub role: AgentRole,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentRole {
    Viewer,
    Operator,
    Planner,
    Administrator,
}

#[derive(Debug, Clone)]
pub enum AgentError {
    ToolFailed(String),
    ConstraintRejected(String),
    Unauthorized,
    Internal(String),
}

pub type AgentResult<T> = std::result::Result<T, AgentError>;

AgentContext

pub struct AgentContext<'a> {
    network: &'a NetworkGraph,
    timeseries: &'a TimeSeriesEngine,
    eventbus: &'a EventBus,
    tools: &'a ToolRegistry,
    memory: &'a mut eneros_memory::MemoryStore,
}

impl<'a> AgentContext<'a> {
    pub async fn get_network(&self) -> &NetworkGraph {
        self.network
    }

    pub async fn call_tool(&self, name: &str, args: &serde_json::Value) -> AgentResult<serde_json::Value> {
        self.tools.call(name, args).await.map_err(AgentError::ToolFailed)
    }

    pub async fn publish(&self, event: impl Into<Event>) {
        self.eventbus.publish(event.into()).await;
    }

    pub async fn remember(&mut self, key: &str, value: &serde_json::Value) -> AgentResult<()> {
        self.memory.store(key, value).await.map_err(|e| AgentError::Internal(e.to_string()))
    }
}

Orchestrator

use std::collections::HashMap;

pub struct Orchestrator {
    agents: HashMap<AgentId, Box<dyn Agent>>,
    schedule: Vec<AgentId>,
    tick_interval: std::time::Duration,
}

impl Default for Orchestrator {
    fn default() -> Self {
        Self {
            agents: HashMap::new(),
            schedule: Vec::new(),
            tick_interval: std::time::Duration::from_millis(100),
        }
    }
}

impl Orchestrator {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn register(&mut self, agent: Box<dyn Agent>) {
        let id = agent.id().clone();
        self.schedule.push(id.clone());
        self.agents.insert(id, agent);
    }

    pub fn unregister(&mut self, id: &AgentId) -> Option<Box<dyn Agent>> {
        self.schedule.retain(|x| x != id);
        self.agents.remove(id)
    }

    pub async fn step(&mut self, ctx: &mut AgentContext<'_>) -> AgentResult<()> {
        for id in self.schedule.clone() {
            if let Some(agent) = self.agents.get_mut(&id) {
                agent.run(ctx).await?;
            }
        }
        Ok(())
    }

    pub async fn run_forever(&mut self, mut ctx: AgentContext<'_>) -> AgentResult<()> {
        loop {
            self.step(&mut ctx).await?;
            tokio::time::sleep(self.tick_interval).await;
        }
    }

    pub fn agent_count(&self) -> usize {
        self.agents.len()
    }
}

Built-in Domain Agents

pub struct DispatchAgent {
    id: AgentId,
    jurisdiction: Jurisdiction,
    state: DispatchState,
}

#[derive(Debug, Clone)]
pub struct DispatchState {
    pub last_dispatch_mw: f64,
    pub last_tick: Option<chrono::DateTime<chrono::Utc>>,
}

impl DispatchAgent {
    pub fn new(id: impl Into<String>) -> Self {
        Self {
            id: AgentId::new(id),
            jurisdiction: Jurisdiction {
                buses: Vec::new(),
                branches: Vec::new(),
                region: None,
                role: AgentRole::Operator,
            },
            state: DispatchState {
                last_dispatch_mw: 0.0,
                last_tick: None,
            },
        }
    }

    pub fn with_jurisdiction(mut self, j: Jurisdiction) -> Self {
        self.jurisdiction = j;
        self
    }
}

#[async_trait]
impl Agent for DispatchAgent {
    fn id(&self) -> &AgentId {
        &self.id
    }

    fn agent_type(&self) -> AgentType {
        AgentType::Dispatch
    }

    fn jurisdiction(&self) -> &Jurisdiction {
        &self.jurisdiction
    }

    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> AgentResult<()> {
        let net = ctx.get_network().await;
        let pf_result = ctx.call_tool("powerflow", &serde_json::json!({"network_id": "default"})).await?;
        let total_mw: f64 = pf_result["total_generation_mw"].as_f64().unwrap_or(0.0);
        self.state.last_dispatch_mw = total_mw;
        self.state.last_tick = Some(chrono::Utc::now());
        Ok(())
    }

    async fn on_event(&mut self, _event: &Event) -> AgentResult<()> {
        Ok(())
    }
}

pub struct SelfHealingAgent {
    id: AgentId,
    jurisdiction: Jurisdiction,
    isolation_strategy: IsolationStrategy,
}

#[derive(Debug, Clone, Copy)]
pub enum IsolationStrategy {
    FastIsolation,
    ConservativeIsolation,
}

impl SelfHealingAgent {
    pub fn new(id: impl Into<String>) -> Self {
        Self {
            id: AgentId::new(id),
            jurisdiction: Jurisdiction {
                buses: Vec::new(),
                branches: Vec::new(),
                region: None,
                role: AgentRole::Operator,
            },
            isolation_strategy: IsolationStrategy::FastIsolation,
        }
    }
}

Core API

MethodSignatureDescription
Orchestrator::newfn new() -> SelfConstruct orchestrator
Orchestrator::registerfn register(&mut self, agent: Box<dyn Agent>)Register Agent
Orchestrator::unregisterfn unregister(&mut self, id: &AgentId) -> Option<Box<dyn Agent>>Unregister Agent
Orchestrator::stepasync fn step(&mut self, ctx: &mut AgentContext) -> AgentResult<()>Single-step execution of all Agents
Orchestrator::run_foreverasync fn run_forever(&mut self, ctx: AgentContext) -> AgentResult<()>Run main loop forever
Agent::runasync fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()>Single execution (trait method)
Agent::on_eventasync fn on_event(&mut self, event: &Event) -> AgentResult<()>Event callback
AgentContext::call_toolasync fn call_tool(&self, name, args) -> AgentResult<Value>Invoke tool
AgentContext::publishasync fn publish(&self, event: impl Into<Event>)Publish event
AgentContext::rememberasync fn remember(&mut self, key, value) -> AgentResult<()>Write to memory

Usage Examples

Register and Run Multiple Agents

use eneros_agent::{Orchestrator, DispatchAgent, SelfHealingAgent, AgentContext};
use std::time::Duration;

let mut orch = Orchestrator::new();
orch.tick_interval = Duration::from_millis(100);

orch.register(Box::new(DispatchAgent::new("dispatch_01")));
orch.register(Box::new(SelfHealingAgent::new("selfheal_01")));

println!("Registered Agent count: {}", orch.agent_count());

let ctx = AgentContext::new(&network, &timeseries, &eventbus, &tools, &mut memory);
orch.run_forever(ctx).await?;

Single-Step Execution

let mut ctx = AgentContext::new(&network, &timeseries, &eventbus, &tools, &mut memory);

for step in 0..10 {
    println!("=== Step {} ===", step);
    orch.step(&mut ctx).await?;
}

Custom Agent

use async_trait::async_trait;
use eneros_agent::{Agent, AgentType, AgentResult, AgentContext, Jurisdiction, AgentRole, AgentError};
use eneros_core::AgentId;

struct VoltageMonitorAgent {
    id: AgentId,
    jurisdiction: Jurisdiction,
}

impl VoltageMonitorAgent {
    fn new(id: impl Into<String>) -> Self {
        Self {
            id: AgentId::new(id),
            jurisdiction: Jurisdiction {
                buses: Vec::new(),
                branches: Vec::new(),
                region: None,
                role: AgentRole::Viewer,
            },
        }
    }
}

#[async_trait]
impl Agent for VoltageMonitorAgent {
    fn id(&self) -> &AgentId {
        &self.id
    }

    fn agent_type(&self) -> AgentType {
        AgentType::Custom
    }

    fn jurisdiction(&self) -> &Jurisdiction {
        &self.jurisdiction
    }

    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> AgentResult<()> {
        let net = ctx.get_network().await;
        let violations = ctx.call_tool("check_constraints", &serde_json::json!({})).await?;

        if violations["count"].as_u64().unwrap_or(0) > 0 {
            ctx.publish(AlarmEvent::critical(violations.clone())).await;
            ctx.remember("last_violation", &violations).await?;
        }
        Ok(())
    }

    async fn on_event(&mut self, event: &Event) -> AgentResult<()> {
        match event {
            Event::TopologyChanged(_) => {
                tracing::info!("Topology changed, re-validating");
            }
            _ => {}
        }
        Ok(())
    }
}

let mut orch = Orchestrator::new();
orch.register(Box::new(VoltageMonitorAgent::new("vm_01")));

Agent with Jurisdiction and Permissions

use eneros_agent::{DispatchAgent, Jurisdiction, AgentRole};
use eneros_core::ElementId;

let jurisdiction = Jurisdiction {
    buses: vec![ElementId::new("bus_1"), ElementId::new("bus_2")],
    branches: vec![ElementId::new("branch_1_2")],
    region: Some("east".into()),
    role: AgentRole::Operator,
};

let agent = DispatchAgent::new("dispatch_east")
    .with_jurisdiction(jurisdiction);

orch.register(Box::new(agent));

Performance Metrics

Test environment: 4 cores / 8GB / Ubuntu 22.04 / Rust 1.78 release build.

OperationLatencyDescription
Agent registration< 5μsHashMap insertion
Single Agent tick (no tool call)< 50μsState update only
Single Agent tick (with Load Flow tool)< 15msMainly Load Flow solving time
1000 Agents single-step scheduling< 5msExcluding tool calls
Event broadcast (1000 subscribers)< 1msEvent bus broadcast

Configuration Parameter Table

[agent] Configuration Section

FieldTypeDefaultDescription
max_concurrentusize64Maximum concurrent Agents
default_tick_interval_msu64100Default tick interval (milliseconds)
memory_retention_hoursu3224Short-term memory retention hours
enable_reasoningbooltrueWhether to enable LLM reasoning

AgentRole Permission Matrix

RoleViewOperatePlanManage
Viewer
Operator
Planner
Administrator

Dependencies

Own Dependencies

[dependencies]
eneros-core = { path = "../core", version = "0.47.0" }
eneros-topology = { path = "../topology", version = "0.47.0" }
eneros-powerflow = { path = "../powerflow", version = "0.47.0" }
eneros-constraint = { path = "../constraint", version = "0.47.0" }
eneros-memory = { path = "../memory", version = "0.47.0" }
eneros-tool = { path = "../tool", version = "0.47.0" }
eneros-reasoning = { path = "../reasoning", version = "0.47.0" }
eneros-eventbus = { path = "../eventbus", version = "0.47.0" }
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] }

Depended On By

  • eneros-gateway — Gateway proxies Agent command dispatch
  • eneros-dashboard — Dashboard displays Agent status
  • eneros-edge — Edge nodes run lightweight Agents
  • eneros-aiops — AIOps integrates Agent decision-making

Version and Compatibility

eneros-agent VersionEnerOS VersionKey Changes
0.47.x0.47.xAdded Jurisdiction and AgentRole permission model
0.46.x0.46.xIntegrated eneros-reasoning LLM reasoning
0.45.x (LTS)0.45.xLTS version, security updates until 2028