Skip to main content

v0.6.0 Release Notes

EnerOS v0.6.0

Release Date: June 2, 2024 Codename: Agent Git Tag: v0.6.0 Support Status: Internal Preview Total Crates: 5 Test Cases: 781

Version Overview

EnerOS v0.6.0 “Agent” is a landmark version that upgrades EnerOS from a “power system analysis library” to an “AgentOS”. This version releases the eneros-agent crate, defining the Agent trait, Agent lifecycle management, message passing mechanism, and state machine. From now on, EnerOS is no longer just a library being called, but a runtime capable of actively hosting and scheduling Agents. Each Agent is an independent async task with its own state, mailbox, and execution loop, able to perceive grid state, make decisions, and execute actions.

The Agent framework design draws inspiration from the Actor model (Erlang/Akka) and ROS 2’s node model, but is deeply adapted for power system scenarios. Each Agent is associated with an “electrical identity” — it can be bound to a bus, a transformer, or an area, and the Agent’s visible scope is determined by its electrical identity. For example, a voltage monitoring Agent bound to bus 1 can only read bus 1’s voltage, not bus 2’s data; while a dispatch Agent bound to an entire electrical island can read all device states within that island. This “electrical identity as permission boundary” design seamlessly integrates with v0.2.0’s capability model, achieving grid-topology-based fine-grained access control.

The Agent state machine of v0.6.0 contains 6 states: Created, Initialized, Running, Suspended, Stopped, and Failed. State transitions are driven by the kernel scheduler; Agents themselves can only request state transitions (such as requesting suspension), and the final approval is decided by the kernel based on global scheduling policies. This design ensures the kernel’s absolute control over Agents — even if an Agent panics, the kernel can transition its state to Failed and clean up resources without affecting other Agents and grid safety.

Key Data

MetricValueDescription
Agent states6Complete lifecycle
Message passing latency P991.8 msSame node
Single-node Agent limit1024Default config
Agent startup time2.1 msIncluding initialization
Message throughput55,000/secSingle Agent
Built-in Agent templates4Monitor/Dispatch/Alert/Inspect

New Features

1. Agent Trait Definition

// crates/eneros-agent/src/lib.rs
use async_trait::async_trait;
use eneros_core::error::EnerOSResult;
use eneros_topology::network::Network;
use std::fmt;

/// Agent unique identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AgentId(pub u64);

impl fmt::Display for AgentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "agent:{}", self.0)
    }
}

/// Agent state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgentState {
    Created,      // Created, not initialized
    Initialized,  // Initialized, not started
    Running,      // Running
    Suspended,    // Suspended
    Stopped,      // Stopped
    Failed,       // Failed
}

/// Agent context: provides kernel service access
pub struct AgentContext<'a> {
    pub agent_id: AgentId,
    pub network: &'a Network,
    pub mailbox: &'a mut Mailbox,
    pub kernel: &'a KernelProxy,
}

/// Agent trait: all Agents must implement
#[async_trait]
pub trait Agent: Send + 'static {
    /// Agent type name
    fn name(&self) -> &str;

    /// Agent description
    fn description(&self) -> &str;

    /// Initialize (state: Created → Initialized)
    async fn init(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()>;

    /// Main loop (state: Initialized → Running)
    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()>;

    /// Suspend (state: Running → Suspended)
    async fn suspend(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()>;

    /// Resume (state: Suspended → Running)
    async fn resume(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()>;

    /// Stop (state: any → Stopped)
    async fn stop(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()>;
}

2. Agent Lifecycle Management

// crates/eneros-agent/src/lifecycle.rs
use crate::{Agent, AgentId, AgentState, AgentContext};
use eneros_core::error::EnerOSResult;
use std::collections::HashMap;
use tokio::task::JoinHandle;

/// Agent registry entry
struct AgentEntry {
    agent: Box<dyn Agent>,
    state: AgentState,
    handle: Option<JoinHandle<EnerOSResult<()>>>,
}

/// Agent manager
pub struct AgentManager {
    agents: HashMap<AgentId, AgentEntry>,
    next_id: u64,
}

impl AgentManager {
    pub fn new() -> Self {
        Self { agents: HashMap::new(), next_id: 1 }
    }

    /// Register a new Agent
    pub fn register(&mut self, mut agent: Box<dyn Agent>) -> AgentId {
        let id = AgentId(self.next_id);
        self.next_id += 1;
        self.agents.insert(id, AgentEntry {
            agent,
            state: AgentState::Created,
            handle: None,
        });
        id
    }

    /// Start an Agent
    pub async fn start(&mut self, id: AgentId, ctx: AgentContext<'_>) -> EnerOSResult<()> {
        let entry = self.agents.get_mut(&id)
            .ok_or(AgentError::NotFound(id))?;
        if entry.state != AgentState::Created && entry.state != AgentState::Initialized {
            return Err(AgentError::InvalidState(entry.state).into());
        }
        entry.agent.init(&mut ctx_clone).await?;
        entry.state = AgentState::Running;
        Ok(())
    }

    /// Stop an Agent
    pub async fn stop(&mut self, id: AgentId, ctx: AgentContext<'_>) -> EnerOSResult<()> {
        let entry = self.agents.get_mut(&id)
            .ok_or(AgentError::NotFound(id))?;
        entry.agent.stop(&mut ctx_clone).await?;
        entry.state = AgentState::Stopped;
        if let Some(handle) = entry.handle.take() {
            handle.abort();
        }
        Ok(())
    }

    /// Get Agent state
    pub fn state(&self, id: AgentId) -> Option<AgentState> {
        self.agents.get(&id).map(|e| e.state)
    }
}

3. Message Passing

Agents communicate through async message passing; each Agent has a Mailbox.

// crates/eneros-agent/src/message.rs
use crate::AgentId;
use eneros_core::error::EnerOSResult;
use tokio::sync::mpsc;
use serde::{Serialize, Deserialize};

/// Message priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessagePriority {
    High,
    Normal,
    Low,
}

/// Agent message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
    pub from: AgentId,
    pub to: AgentId,
    pub priority: MessagePriority,
    pub payload: MessagePayload,
    pub timestamp: i64,
}

/// Message payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessagePayload {
    Text(String),
    Command(String),
    Data(serde_json::Value),
    Event(String, serde_json::Value),
    Query(String),
    Response(serde_json::Value),
}

/// Agent mailbox
pub struct Mailbox {
    receiver: mpsc::Receiver<Message>,
    sender: mpsc::Sender<Message>,
}

impl Mailbox {
    pub fn new(capacity: usize) -> Self {
        let (sender, receiver) = mpsc::channel(capacity);
        Self { receiver, sender }
    }

    /// Receive message (blocking)
    pub async fn recv(&mut self) -> Option<Message> {
        self.receiver.recv().await
    }

    /// Send message
    pub async fn send(&self, msg: Message) -> EnerOSResult<()> {
        self.sender.send(msg).await
            .map_err(|_| AgentError::MailboxClosed.into())
    }

    /// Non-blocking try receive
    pub fn try_recv(&mut self) -> Option<Message> {
        self.receiver.try_recv().ok()
    }
}

4. Agent State Machine

// crates/eneros-agent/src/state_machine.rs
use crate::{AgentState, AgentId};

/// State transition event
#[derive(Debug, Clone, Copy)]
pub enum StateEvent {
    Init,      // Created → Initialized
    Start,     // Initialized → Running
    Suspend,   // Running → Suspended
    Resume,    // Suspended → Running
    Stop,      // any → Stopped
    Fail,      // any → Failed
}

/// State machine
pub struct AgentStateMachine {
    state: AgentState,
}

impl AgentStateMachine {
    pub fn new() -> Self {
        Self { state: AgentState::Created }
    }

    pub fn state(&self) -> AgentState {
        self.state
    }

    /// Attempt state transition
    pub fn transition(&mut self, event: StateEvent) -> Result<AgentState, AgentError> {
        let new_state = match (self.state, event) {
            (AgentState::Created, StateEvent::Init) => AgentState::Initialized,
            (AgentState::Initialized, StateEvent::Start) => AgentState::Running,
            (AgentState::Running, StateEvent::Suspend) => AgentState::Suspended,
            (AgentState::Suspended, StateEvent::Resume) => AgentState::Running,
            (_, StateEvent::Stop) => AgentState::Stopped,
            (_, StateEvent::Fail) => AgentState::Failed,
            (s, e) => return Err(AgentError::InvalidTransition(s, e)),
        };
        self.state = new_state;
        Ok(new_state)
    }
}

State transition diagram:

Current StateEventNew State
CreatedInitInitialized
InitializedStartRunning
RunningSuspendSuspended
SuspendedResumeRunning
anyStopStopped
anyFailFailed

5. Built-in Agent Templates

// crates/eneros-agent/src/templates/monitor.rs
use crate::{Agent, AgentContext, AgentId};
use eneros_core::error::EnerOSResult;
use async_trait::async_trait;

/// Voltage monitoring Agent
pub struct VoltageMonitorAgent {
    bus_id: eneros_topology::bus::BusId,
    threshold: f64,
}

impl VoltageMonitorAgent {
    pub fn new(bus_id: eneros_topology::bus::BusId) -> Self {
        Self { bus_id, threshold: 0.95 }
    }
}

#[async_trait]
impl Agent for VoltageMonitorAgent {
    fn name(&self) -> &str { "voltage_monitor" }
    fn description(&self) -> &str { "Bus voltage monitoring Agent" }

    async fn init(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> {
        tracing::info!(bus = %self.bus_id, "Voltage monitor Agent initialized");
        Ok(())
    }

    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()> {
        loop {
            // Read bus voltage via syscall
            let voltage = ctx.kernel.read_bus_voltage(self.bus_id).await?;
            if voltage < self.threshold {
                // Send alert message
                ctx.mailbox.send(Message {
                    from: ctx.agent_id,
                    to: AgentId(0), // Dispatch center
                    priority: MessagePriority::High,
                    payload: MessagePayload::Event("voltage_low".into(),
                        serde_json::json!({"bus": self.bus_id, "voltage": voltage})),
                    timestamp: chrono::Utc::now().timestamp(),
                }).await?;
            }
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }
    }

    async fn suspend(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
    async fn resume(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
    async fn stop(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
}

Usage example:

use eneros_agent::{AgentManager, templates::VoltageMonitorAgent};
use eneros_topology::bus::BusId;

let mut manager = AgentManager::new();
let agent = Box::new(VoltageMonitorAgent::new(BusId(1)));
let id = manager.register(agent);
manager.start(id, ctx).await?;

println!("Agent {} state: {:?}", id, manager.state(id).unwrap());

Improvements

  • eneros-core: Added KernelProxy type for Agents to interact with the kernel
  • eneros-topology: Added Network::base_mva() method
  • Dependencies: Introduced tokio, async-trait, chrono
  • CI: Added cargo nextest concurrent testing

Bug Fixes

  • Fixed Mailbox::send panicking when the receiving end is closed (#91)
  • Fixed AgentManager::start not rejecting when Agent is already running (#95)
  • Fixed AgentStateMachine::transition still accepting Stop in Failed state (#98)

Breaking Changes

  • eneros_core::Kernel: Added KernelProxy view type; Agents no longer directly hold &Kernel

Performance Improvements

OperationTimeThroughput
Agent registration1.2 μs-
Agent startup2.1 ms-
Message passing (same node) P991.8 ms55,000/sec
State machine transition18 ns-
1024 Agent memory usage48 MB-

Message passing latency distribution:

PercentileLatency
P50420 μs
P901.1 ms
P991.8 ms
P99.93.2 ms

Contributors

ContributorRoleCommits
@eneros-foundationArchitect38
@actor-model-expertActor Model Expert35
@async-rustaceanAsync Rust Engineer41
@ros2-contributorROS 2 Experience12

Upgrade Guide

New Dependencies

[dependencies]
eneros-agent = { version = "0.6", path = "../eneros-agent" }
tokio = { version = "1.35", features = ["full"] }
async-trait = "0.1"

Implement Custom Agent

use eneros_agent::{Agent, AgentContext};
use async_trait::async_trait;

struct MyAgent;

#[async_trait]
impl Agent for MyAgent {
    fn name(&self) -> &str { "my_agent" }
    fn description(&self) -> &str { "Custom Agent" }

    async fn init(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> {
        Ok(())
    }

    async fn run(&mut self, ctx: &mut AgentContext<'_>) -> EnerOSResult<()> {
        while let Some(msg) = ctx.mailbox.recv().await {
            // Handle message
        }
        Ok(())
    }

    async fn suspend(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
    async fn resume(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
    async fn stop(&mut self, _ctx: &mut AgentContext<'_>) -> EnerOSResult<()> { Ok(()) }
}