First Agent
This section guides you through creating a complete DispatchAgent that can listen to load changes, forecast future load, perform economic dispatch, and issue dispatch commands. This section covers Agent basic structure, lifecycle, tool registration, execution methods, and debugging tips.
Agent Basic Structure
All Agents in EnerOS implement the eneros_agent::Agent trait. An Agent typically consists of the following components:
| Component | Description |
|---|---|
| Name and ID | The Agent’s unique identifier in the kernel |
| Tool Registry (ToolRegistry) | The set of tools the Agent can call |
| Context (AgentContext) | Provides runtime capabilities such as measurement, commands, and memory |
run method | The Agent’s main loop logic |
Below is a complete DispatchAgent implementation, including tool definitions, Agent implementation, and main function entry.
Complete DispatchAgent Example
Save the following code as examples/dispatch_agent.rs:
use eneros_agent::{Agent, AgentContext, AgentResult, AgentStatus};
use eneros_tool::{Tool, ToolRegistry, ToolError, ToolOutput};
use serde::{Deserialize, Serialize};
// ============================================================
// Tool 1: Load Forecast Tool
// ============================================================
#[derive(Debug, Serialize, Deserialize)]
struct LoadForecastInput {
current_load_mw: f64,
horizon_minutes: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct LoadForecastOutput {
forecast_mw: Vec<f64>,
confidence: f64,
}
struct LoadForecastTool;
#[async_trait::async_trait]
impl Tool for LoadForecastTool {
fn name(&self) -> &str {
"load_forecast"
}
fn description(&self) -> &str {
"Forecast the load curve for a future period based on the current load"
}
async fn execute(&self, input: serde_json::Value) -> Result<ToolOutput, ToolError> {
let input: LoadForecastInput = serde_json::from_value(input)
.map_err(|e| ToolError::InvalidInput(e.to_string()))?;
// Simplified example: linear extrapolation + noise
let mut forecast = Vec::with_capacity(input.horizon_minutes as usize);
for i in 0..input.horizon_minutes {
let t = (i + 1) as f64;
let value = input.current_load_mw * (1.0 + 0.001 * t.sin());
forecast.push(value);
}
let output = LoadForecastOutput {
forecast_mw: forecast,
confidence: 0.92,
};
Ok(ToolOutput::json(output))
}
}
// ============================================================
// Tool 2: Economic Dispatch Tool
// ============================================================
#[derive(Debug, Serialize, Deserialize)]
struct EconomicDispatchInput {
forecast_mw: Vec<f64>,
generators: Vec<Generator>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Generator {
name: String,
pmin_mw: f64,
pmax_mw: f64,
cost_per_mwh: f64,
}
#[derive(Debug, Serialize, Deserialize)]
struct EconomicDispatchOutput {
dispatch: Vec<GeneratorDispatch>,
total_cost: f64,
}
#[derive(Debug, Serialize, Deserialize)]
struct GeneratorDispatch {
name: String,
output_mw: f64,
}
struct EconomicDispatchTool;
#[async_trait::async_trait]
impl Tool for EconomicDispatchTool {
fn name(&self) -> &str {
"economic_dispatch"
}
fn description(&self) -> &str {
"Perform economic dispatch based on load forecast and generator parameters (allocated by capacity ratio)"
}
async fn execute(&self, input: serde_json::Value) -> Result<ToolOutput, ToolError> {
let input: EconomicDispatchInput = serde_json::from_value(input)
.map_err(|e| ToolError::InvalidInput(e.to_string()))?;
let total_load: f64 = input.forecast_mw.iter().sum::<f64>()
/ input.forecast_mw.len().max(1) as f64;
// Simplified example: allocate by capacity ratio, without cost optimization
let total_capacity: f64 = input.generators.iter()
.map(|g| g.pmax_mw - g.pmin_mw)
.sum();
let mut dispatch = Vec::new();
let mut total_cost = 0.0;
for gen in &input.generators {
let share = (gen.pmax_mw - gen.pmin_mw) / total_capacity.max(1e-9);
let output_mw = (gen.pmin_mw + share * (total_load - input.generators.iter().map(|g| g.pmin_mw).sum::<f64>()))
.clamp(gen.pmin_mw, gen.pmax_mw);
total_cost += output_mw * gen.cost_per_mwh;
dispatch.push(GeneratorDispatch {
name: gen.name.clone(),
output_mw,
});
}
let output = EconomicDispatchOutput {
dispatch,
total_cost,
};
Ok(ToolOutput::json(output))
}
}
// ============================================================
// Agent Implementation
// ============================================================
struct DispatchAgent {
name: String,
tools: ToolRegistry,
}
impl DispatchAgent {
fn new() -> Self {
let mut tools = ToolRegistry::new();
tools.register(LoadForecastTool);
tools.register(EconomicDispatchTool);
Self {
name: "dispatch-agent".to_string(),
tools,
}
}
fn with_generators() -> Self {
let mut agent = Self::new();
// Generator information can be preset here during Agent initialization
agent
}
}
#[async_trait::async_trait]
impl Agent for DispatchAgent {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> &str {
"0.1.0"
}
async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
ctx.log_info("DispatchAgent started").await?;
// 1. Get current load
let load = ctx
.get_measurement("total_load")
.await?
.ok_or_else(|| eneros_agent::AgentError::MeasurementNotFound("total_load".into()))?;
ctx.log_info(&format!("Current load: {:.2} MW", load)).await?;
// 2. Forecast load for the next 60 minutes
let forecast_input = LoadForecastInput {
current_load_mw: load,
horizon_minutes: 60,
};
let forecast_output: LoadForecastOutput = self
.tools
.call_json("load_forecast", forecast_input)
.await?;
ctx.log_info(&format!(
"Load forecast completed, confidence: {:.2}",
forecast_output.confidence
))
.await?;
// 3. Perform economic dispatch
let dispatch_input = EconomicDispatchInput {
forecast_mw: forecast_output.forecast_mw.clone(),
generators: vec![
Generator {
name: "G1".to_string(),
pmin_mw: 10.0,
pmax_mw: 100.0,
cost_per_mwh: 50.0,
},
Generator {
name: "G2".to_string(),
pmin_mw: 5.0,
pmax_mw: 80.0,
cost_per_mwh: 65.0,
},
Generator {
name: "G3".to_string(),
pmin_mw: 0.0,
pmax_mw: 50.0,
cost_per_mwh: 80.0,
},
],
};
let dispatch_output: EconomicDispatchOutput = self
.tools
.call_json("economic_dispatch", dispatch_input)
.await?;
// 4. Issue dispatch commands
for gen in &dispatch_output.dispatch {
ctx.send_command(
"set_generation",
serde_json::json!({
"generator": gen.name,
"output_mw": gen.output_mw
}),
)
.await?;
}
ctx.log_info(&format!(
"Dispatch completed, total cost: {:.2}",
dispatch_output.total_cost
))
.await?;
Ok(())
}
}
// ============================================================
// Main Function
// ============================================================
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
tracing_subscriber::fmt::init();
// Create Agent context
let mut ctx = AgentContext::builder()
.agent_id("dispatch-001")
.measurement_endpoint("http://localhost:8080/api/v1")
.command_endpoint("http://localhost:8080/api/v1")
.build();
// Set current load (for demonstration)
ctx.set_measurement("total_load", 150.0).await?;
// Create and run the Agent
let mut agent = DispatchAgent::with_generators();
let status = agent.run(&mut ctx).await;
match status {
Ok(()) => println!("Agent ran successfully"),
Err(e) => println!("Agent run failed: {:?}", e),
}
Ok(())
}
Add the above file to examples/dispatch_agent.rs, confirm that eneros-agent and eneros-tool are declared in [workspace.dependencies] of the root Cargo.toml, then run:
cargo run --release --example dispatch_agent
Expected output:
2026-07-06T10:00:00 INFO dispatch_agent: DispatchAgent started
2026-07-06T10:00:00 INFO dispatch_agent: Current load: 150.00 MW
2026-07-06T10:00:00 INFO dispatch_agent: Load forecast completed, confidence: 0.92
2026-07-06T10:00:01 INFO dispatch_agent: Dispatch completed, total cost: 8250.00
Agent ran successfully
Agent Lifecycle
The complete lifecycle of an EnerOS Agent includes the following phases:
| Phase | State | Description |
|---|---|---|
| Register | Registered | Agent is registered with the runtime but not started |
| Initialize | Initializing | init() method called, loading tools and configuration |
| Ready | Ready | Initialization complete, waiting for scheduling |
| Running | Running | run() method called to execute main logic |
| Paused | Paused | Paused externally, can be resumed |
| Stopped | Stopped | Actively stopped, resources released |
| Error | Error | Abnormal exit |
| Completed | Completed | Main logic completed normally |
State Transition Diagram
Registered -> Initializing -> Ready -> Running -> Completed
^ |
| v
+------ Paused
|
v
Stopped
^
|
Running -> Error
Lifecycle Hooks
#[async_trait::async_trait]
impl Agent for DispatchAgent {
async fn init(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
// Load historical data during initialization
ctx.log_info("Initializing DispatchAgent").await?;
Ok(())
}
async fn run(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
// Main logic
Ok(())
}
async fn pause(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
// Save context when paused
ctx.save_checkpoint("dispatch_pause").await?;
Ok(())
}
async fn resume(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
// Load context when resumed
ctx.load_checkpoint("dispatch_pause").await?;
Ok(())
}
async fn stop(&mut self, ctx: &mut AgentContext) -> AgentResult<()> {
// Clean up resources when stopped
ctx.log_info("DispatchAgent stopped").await?;
Ok(())
}
}
Tool Registration Details
ToolRegistry is the Agent’s tool container, supporting registration, querying, and invocation.
Register Tools
use eneros_tool::ToolRegistry;
let mut tools = ToolRegistry::new();
// Register a single tool
tools.register(LoadForecastTool);
tools.register(EconomicDispatchTool);
// Register in batch
tools.register_all(vec![
Box::new(LoadForecastTool),
Box::new(EconomicDispatchTool),
]);
Query Tools
// List all tools
for name in tools.list() {
println!("Registered tool: {}", name);
}
// Check if a tool exists
if tools.contains("load_forecast") {
println!("Load forecast tool available");
}
// Get tool description
if let Some(desc) = tools.description("economic_dispatch") {
println!("Description: {}", desc);
}
Invoke Tools
// Directly call and parse output
let output: LoadForecastOutput = tools
.call_json("load_forecast", LoadForecastInput {
current_load_mw: 150.0,
horizon_minutes: 60,
})
.await?;
// Get raw ToolOutput
let raw = tools.call("load_forecast", json!({"current_load_mw": 150.0})).await?;
println!("Output type: {:?}", raw.kind);
Built-in Toolset
EnerOS provides 30+ built-in tools in the eneros-tool crate:
| Tool | Category | Description |
|---|---|---|
load_forecast | Forecast | Load forecasting |
generation_forecast | Forecast | Renewable energy output forecasting |
economic_dispatch | Optimization | Economic dispatch |
optimal_power_flow | Optimization | Optimal power flow |
contingency_analysis | Safety | N-1 contingency analysis |
state_estimation | Estimation | State estimation |
topology_analysis | Topology | Topology analysis |
fault_location | Ops | Fault location |
equipment_health | Ops | Equipment health assessment |
market_clearing | Trading | Market clearing |
demand_response | Efficiency | Demand response |
weather_query | Data | Weather query |
Run the Agent
Run via CLI
# Run a built-in Agent
./target/release/enerosctl agent run dispatch-agent
# Specify Agent ID
./target/release/enerosctl agent run dispatch-agent --id dispatch-001
# Specify configuration
./target/release/enerosctl agent run dispatch-agent --config agent.toml
# List all registered Agents
./target/release/enerosctl agent list
# View Agent status
./target/release/enerosctl agent status dispatch-001
# Stop an Agent
./target/release/enerosctl agent stop dispatch-001
Run via REST API
# Create and run an Agent
curl -X POST http://localhost:8080/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"type": "dispatch",
"id": "dispatch-001",
"config": {
"generators": [
{"name": "G1", "pmin_mw": 10, "pmax_mw": 100, "cost_per_mwh": 50},
{"name": "G2", "pmin_mw": 5, "pmax_mw": 80, "cost_per_mwh": 65}
]
}
}'
Response:
{
"agent_id": "dispatch-001",
"status": "running",
"started_at": "2026-07-06T10:00:00Z"
}
# View Agent status
curl http://localhost:8080/api/v1/agents/dispatch-001
# Stop an Agent
curl -X DELETE http://localhost:8080/api/v1/agents/dispatch-001
# List all Agents
curl http://localhost:8080/api/v1/agents
Run via Rust Code
use eneros_agent::{AgentRuntime, AgentRuntimeBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create runtime
let mut runtime: AgentRuntime = AgentRuntimeBuilder::new()
.api_endpoint("http://localhost:8080/api/v1")
.max_agents(100)
.build();
// Register and start the Agent
let agent = DispatchAgent::with_generators();
let agent_id = runtime.register(agent).await?;
runtime.start(&agent_id).await?;
// Wait for completion
runtime.wait(&agent_id).await?;
println!("Agent {} completed", agent_id);
Ok(())
}
Built-in Agent Types
EnerOS provides 7 domain Agents in the eneros-agent crate, ready to use out of the box:
| Agent | Type String | Responsibility | Typical Scenario |
|---|---|---|---|
| DispatchAgent | dispatch | Economic dispatch and load allocation | Real-time dispatch, day-ahead planning |
| SelfHealingAgent | self-healing | Fault location, isolation, and restoration (FDIR) | Distribution network self-healing |
| OperationAgent | operation | Equipment condition monitoring and Ops | Equipment health management |
| PlanningAgent | planning | Distribution network planning and scheme comparison | Distribution network expansion |
| TradingAgent | trading | Power market trading | Spot market, virtual power plant |
| EnergyAgent | energy | Energy efficiency optimization and demand response | Demand response, energy efficiency management |
| Orchestrator | orchestrator | Multi-agent orchestration and coordination | Cross-region collaboration |
Built-in Agent Configuration Examples
# Run a self-healing Agent
curl -X POST http://localhost:8080/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"type": "self-healing",
"id": "sh-001",
"config": {
"network_id": "net_a1b2c3d4",
"auto_isolate": true,
"auto_restore": true,
"max_restore_time_s": 60
}
}'
# Run a trading Agent
curl -X POST http://localhost:8080/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"type": "trading",
"id": "trade-001",
"config": {
"market_endpoint": "https://market.example.com/api",
"strategy": "price_taker",
"max_bid_mw": 50
}
}'
Agent State Management
Persistent State
EnerOS automatically persists Agent state to SQLite for recovery after restart:
// Save state in run
ctx.save_state("last_dispatch", &dispatch_output).await?;
// Load after restart
let last: EconomicDispatchOutput = ctx.load_state("last_dispatch").await?
.unwrap_or_default();
Checkpoints
// Create a checkpoint
ctx.save_checkpoint("before_dispatch").await?;
// Execute an operation that may fail
let result = self.tools.call("economic_dispatch", input).await;
if result.is_err() {
// Roll back to the checkpoint
ctx.load_checkpoint("before_dispatch").await?;
}
Memory
// Write memory
ctx.remember("event", "Load suddenly increased by 20MW").await?;
// Query memory
let events = ctx.recall("event", 10).await?; // Most recent 10 entries
for ev in events {
println!("[{}] {}", ev.timestamp, ev.content);
}
Debugging Tips
1. Enable Trace-Level Logging
ENEROS_LOG=trace cargo run --example dispatch_agent
2. Single-Step Tool Execution
// Temporarily print tool call details
let raw = tools.call("load_forecast", input).await?;
println!("Tool output: {}", serde_json::to_string_pretty(&raw)?);
3. Use Mock Context
use eneros_agent::MockAgentContext;
let mut mock = MockAgentContext::new();
mock.set_measurement("total_load", 150.0).await;
mock.expect_command("set_generation").times(3);
let mut agent = DispatchAgent::new();
agent.run(&mut mock).await.unwrap();
4. View Agent Logs
# View Agent logs in real time
curl -N http://localhost:8080/api/v1/agents/dispatch-001/logs
# View historical logs
curl "http://localhost:8080/api/v1/agents/dispatch-001/logs?limit=100&level=info"
5. Debug via Dashboard
Visit http://localhost:8080/dashboard/agents to:
- View the list and status of all Agents
- Click an Agent to view real-time logs
- View tool call statistics
- Manually trigger Agent runs
Next Steps
- Configuration File - Understand eneros.toml configuration options