Skip to main content

First Agent

Quick Start

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:

ComponentDescription
Name and IDThe 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 methodThe 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:

PhaseStateDescription
RegisterRegisteredAgent is registered with the runtime but not started
InitializeInitializinginit() method called, loading tools and configuration
ReadyReadyInitialization complete, waiting for scheduling
RunningRunningrun() method called to execute main logic
PausedPausedPaused externally, can be resumed
StoppedStoppedActively stopped, resources released
ErrorErrorAbnormal exit
CompletedCompletedMain 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:

ToolCategoryDescription
load_forecastForecastLoad forecasting
generation_forecastForecastRenewable energy output forecasting
economic_dispatchOptimizationEconomic dispatch
optimal_power_flowOptimizationOptimal power flow
contingency_analysisSafetyN-1 contingency analysis
state_estimationEstimationState estimation
topology_analysisTopologyTopology analysis
fault_locationOpsFault location
equipment_healthOpsEquipment health assessment
market_clearingTradingMarket clearing
demand_responseEfficiencyDemand response
weather_queryDataWeather 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:

AgentType StringResponsibilityTypical Scenario
DispatchAgentdispatchEconomic dispatch and load allocationReal-time dispatch, day-ahead planning
SelfHealingAgentself-healingFault location, isolation, and restoration (FDIR)Distribution network self-healing
OperationAgentoperationEquipment condition monitoring and OpsEquipment health management
PlanningAgentplanningDistribution network planning and scheme comparisonDistribution network expansion
TradingAgenttradingPower market tradingSpot market, virtual power plant
EnergyAgentenergyEnergy efficiency optimization and demand responseDemand response, energy efficiency management
OrchestratororchestratorMulti-agent orchestration and coordinationCross-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