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
| Metric | Value | Description |
|---|---|---|
| Agent states | 6 | Complete lifecycle |
| Message passing latency P99 | 1.8 ms | Same node |
| Single-node Agent limit | 1024 | Default config |
| Agent startup time | 2.1 ms | Including initialization |
| Message throughput | 55,000/sec | Single Agent |
| Built-in Agent templates | 4 | Monitor/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 State | Event | New State |
|---|---|---|
| Created | Init | Initialized |
| Initialized | Start | Running |
| Running | Suspend | Suspended |
| Suspended | Resume | Running |
| any | Stop | Stopped |
| any | Fail | Failed |
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: AddedKernelProxytype for Agents to interact with the kerneleneros-topology: AddedNetwork::base_mva()method- Dependencies: Introduced
tokio,async-trait,chrono - CI: Added
cargo nextestconcurrent testing
Bug Fixes
- Fixed
Mailbox::sendpanicking when the receiving end is closed (#91) - Fixed
AgentManager::startnot rejecting when Agent is already running (#95) - Fixed
AgentStateMachine::transitionstill accepting Stop in Failed state (#98)
Breaking Changes
eneros_core::Kernel: AddedKernelProxyview type; Agents no longer directly hold&Kernel
Performance Improvements
| Operation | Time | Throughput |
|---|---|---|
| Agent registration | 1.2 μs | - |
| Agent startup | 2.1 ms | - |
| Message passing (same node) P99 | 1.8 ms | 55,000/sec |
| State machine transition | 18 ns | - |
| 1024 Agent memory usage | 48 MB | - |
Message passing latency distribution:
| Percentile | Latency |
|---|---|
| P50 | 420 μs |
| P90 | 1.1 ms |
| P99 | 1.8 ms |
| P99.9 | 3.2 ms |
Contributors
| Contributor | Role | Commits |
|---|---|---|
| @eneros-foundation | Architect | 38 |
| @actor-model-expert | Actor Model Expert | 35 |
| @async-rustacean | Async Rust Engineer | 41 |
| @ros2-contributor | ROS 2 Experience | 12 |
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(()) }
}