Extending Agents#
This guide covers how to create custom agents for your domain.
Agent Base Class#
All agents inherit from the base Agent class:
from heron.agents.base import Agent
from heron.core.observation import Observation
from heron.core.action import Action
class Agent(ABC):
def __init__(self, agent_id: str, level: int = 1):
self.agent_id = agent_id
self.level = level
@abstractmethod
def observe(self) -> Observation:
"""Generate observation from current state."""
pass
@abstractmethod
def act(self, observation: Observation, **kwargs) -> Action:
"""Process observation and return action."""
pass
@abstractmethod
def reset(self, seed: int = None):
"""Reset agent state."""
pass
Creating a Custom Field Agent#
Field agents are leaf-level agents with local sensing and actuation:
from heron.agents import FieldAgent
from heron.core.state import FieldAgentState
from heron.core.action import Action
from heron.core.observation import Observation
import numpy as np
class TemperatureSensor(FieldAgent):
"""Custom field agent for temperature monitoring."""
def __init__(self, agent_id: str, location: str):
super().__init__(agent_id, level=1)
self.location = location
# Initialize state
self.state = FieldAgentState()
self.temp_feature = TemperatureFeature(20.0)
self.state.add_feature("temperature", self.temp_feature)
# Initialize action (setpoint control)
self.action = Action()
self.action.set_specs(
dim_c=1,
range=(np.array([15.0]), np.array([30.0]))
)
def observe(self) -> Observation:
return Observation(
local={"state": self.state.vector()},
timestamp=self.current_time
)
def act(self, observation: Observation, **kwargs) -> Action:
# Implement control logic
current_temp = observation.local["state"][0]
setpoint = kwargs.get("setpoint", 22.0)
# Simple proportional control
error = setpoint - current_temp
self.action.c[0] = np.clip(current_temp + 0.5 * error, 15.0, 30.0)
return self.action
def reset(self, seed=None):
self.temp_feature.value = 20.0
self.action.reset()
Creating a Custom Coordinator Agent#
Coordinator agents manage subordinate agents:
from heron.agents import CoordinatorAgent
from heron.protocols.vertical import SetpointProtocol
class ZoneController(CoordinatorAgent):
"""Coordinator for multiple temperature sensors."""
def __init__(self, agent_id: str, sensors: dict):
super().__init__(
agent_id=agent_id,
level=2,
subordinates=sensors
)
self.protocol = SetpointProtocol()
self.target_temperature = 22.0
def observe(self) -> Observation:
# Aggregate observations from subordinates
local_obs = {}
for sub_id, sub_agent in self.subordinates.items():
sub_obs = sub_agent.observe()
local_obs[sub_id] = sub_obs.local["state"]
# Compute zone average
temps = [obs[0] for obs in local_obs.values()]
zone_avg = np.mean(temps)
return Observation(
local={"zone_avg": np.array([zone_avg])},
subordinate_obs=local_obs
)
def act(self, observation: Observation, **kwargs) -> Action:
# Generate setpoints for subordinates
zone_avg = observation.local["zone_avg"][0]
error = self.target_temperature - zone_avg
# Distribute setpoints
setpoints = {}
for sub_id in self.subordinates:
setpoints[sub_id] = self.target_temperature + 0.3 * error
# Execute protocol
self.protocol.execute(self.subordinates, setpoints)
return self.action
def reset(self, seed=None):
for sub in self.subordinates.values():
sub.reset(seed)
Adding Custom Features#
Create domain-specific features by extending Feature:
from heron.core.feature import Feature
import numpy as np
class HumidityFeature(Feature):
"""Humidity measurement feature."""
def __init__(self, initial_humidity: float = 50.0):
super().__init__(visibility=["owner", "coordinator"])
self.humidity = initial_humidity
def vector(self) -> np.ndarray:
return np.array([self.humidity], dtype=np.float32)
def dim(self) -> int:
return 1
def update(self, new_humidity: float):
self.humidity = np.clip(new_humidity, 0.0, 100.0)
Message-Based Agents (Distributed Mode)#
For distributed execution, use Proxy:
from heron.agents import Proxy
from heron.messaging.memory import InMemoryBroker
broker = InMemoryBroker()
# Create actual agent
sensor = TemperatureSensor("sensor_1", "room_a")
# Wrap with proxy for message-based communication
proxy = Proxy(
agent_id="sensor_1_proxy",
actual_agent=sensor,
broker=broker,
upstream_id="zone_controller"
)
# Distributed step
await proxy.step_distributed()
Best Practices#
Keep agents focused: Each agent should have a single responsibility
Use features for state: Compose state from Features
Define clear visibility: Control information sharing with visibility tags
Implement reset properly: Ensure deterministic behavior with seeds
Document action spaces: Clearly define action dimensions and ranges