Agent.java
package org.amcp.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Base class for all AMCP agents.
*
* Provides core functionality for agent lifecycle management,
* event handling, and communication within the agent mesh.
*
* @author AMCP Development Team
* @version 1.5.0
* @since 1.0.0
*/
public abstract class Agent {
private static final Logger logger = LoggerFactory.getLogger(Agent.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private final String agentId;
private final String agentType;
private final Map<String, Object> metadata;
private final List<String> subscriptions;
private final Map<String, String> capabilities;
private AgentState state;
private AgentContext context;
private Instant activationTime;
private Instant lastActivityTime;
/**
* Creates a new agent with the specified ID and type.
*
* @param agentId unique identifier for this agent
* @param agentType the type/class of this agent
*/
protected Agent(String agentId, String agentType) {
this.agentId = Objects.requireNonNull(agentId, "Agent ID cannot be null");
this.agentType = Objects.requireNonNull(agentType, "Agent type cannot be null");
this.metadata = new ConcurrentHashMap<>();
this.subscriptions = new CopyOnWriteArrayList<>();
this.capabilities = new ConcurrentHashMap<>();
this.state = AgentState.CREATED;
logger.info("Agent created: {} [{}]", agentId, agentType);
}
/**
* Gets the unique identifier for this agent.
*
* @return the agent ID
*/
public final String getAgentId() {
return agentId;
}
/**
* Gets the type of this agent.
*
* @return the agent type
*/
public final String getAgentType() {
return agentType;
}
/**
* Gets the current state of this agent.
*
* @return the agent state
*/
public final AgentState getState() {
return state;
}
/**
* Gets the agent context for mesh communication.
*
* @return the agent context
*/
protected final AgentContext getContext() {
return context;
}
/**
* Sets the agent context. Called by the AMCP framework.
*
* @param context the agent context
*/
public final void setContext(AgentContext context) {
this.context = context;
}
/**
* Activates this agent within the mesh.
*
* This method is called by the AMCP framework when the agent
* is being activated. Subclasses should override {@link #onActivation()}
* to perform custom initialization.
*/
public final void activate() {
if (state != AgentState.CREATED) {
throw new IllegalStateException("Agent can only be activated from CREATED state");
}
state = AgentState.ACTIVATING;
activationTime = Instant.now();
lastActivityTime = activationTime;
try {
onActivation();
state = AgentState.ACTIVE;
logger.info("Agent activated: {} [{}]", agentId, agentType);
} catch (Exception e) {
state = AgentState.ERROR;
logger.error("Failed to activate agent: {} [{}]", agentId, agentType, e);
throw new AgentActivationException("Failed to activate agent: " + agentId, e);
}
}
/**
* Deactivates this agent from the mesh.
*
* This method is called by the AMCP framework when the agent
* is being deactivated. Subclasses should override {@link #onDeactivation()}
* to perform custom cleanup.
*/
public final void deactivate() {
if (state != AgentState.ACTIVE) {
logger.warn("Attempting to deactivate agent in state: {}", state);
}
state = AgentState.DEACTIVATING;
try {
onDeactivation();
state = AgentState.DEACTIVATED;
logger.info("Agent deactivated: {} [{}]", agentId, agentType);
} catch (Exception e) {
state = AgentState.ERROR;
logger.error("Failed to deactivate agent: {} [{}]", agentId, agentType, e);
throw new AgentDeactivationException("Failed to deactivate agent: " + agentId, e);
}
}
/**
* Called when the agent is being activated.
*
* Subclasses should override this method to perform custom
* initialization such as subscribing to topics and announcing
* capabilities.
*/
protected void onActivation() {
// Default implementation does nothing
}
/**
* Called when the agent is being deactivated.
*
* Subclasses should override this method to perform custom
* cleanup such as unsubscribing from topics and releasing
* resources.
*/
protected void onDeactivation() {
// Default implementation does nothing
}
/**
* Subscribes to events on the specified topic.
*
* @param topic the topic to subscribe to (supports wildcards)
*/
protected final void subscribe(String topic) {
Objects.requireNonNull(topic, "Topic cannot be null");
if (!subscriptions.contains(topic)) {
subscriptions.add(topic);
if (context != null) {
context.subscribe(this, topic);
}
logger.debug("Agent {} subscribed to topic: {}", agentId, topic);
}
}
/**
* Unsubscribes from events on the specified topic.
*
* @param topic the topic to unsubscribe from
*/
protected final void unsubscribe(String topic) {
Objects.requireNonNull(topic, "Topic cannot be null");
if (subscriptions.remove(topic)) {
if (context != null) {
context.unsubscribe(this, topic);
}
logger.debug("Agent {} unsubscribed from topic: {}", agentId, topic);
}
}
/**
* Publishes an event to the mesh.
*
* @param event the event to publish
*/
public final void publish(Event event) {
Objects.requireNonNull(event, "Event cannot be null");
if (context == null) {
throw new IllegalStateException("Agent context not set");
}
// Set sender if not already set
if (event.getSender() == null) {
event = event.toBuilder().sender(agentId).build();
}
context.publish(event);
lastActivityTime = Instant.now();
logger.debug("Agent {} published event to topic: {}", agentId, event.getTopic());
}
/**
* Announces a capability of this agent.
*
* @param capability the capability name
* @param version the capability version
*/
protected final void announceCapability(String capability, String version) {
Objects.requireNonNull(capability, "Capability cannot be null");
Objects.requireNonNull(version, "Version cannot be null");
capabilities.put(capability, version);
if (context != null) {
Event capabilityEvent = Event.builder()
.topic("system.capability.announce")
.payload(Map.of(
"agentId", agentId,
"capability", capability,
"version", version
))
.sender(agentId)
.build();
context.publish(capabilityEvent);
}
logger.debug("Agent {} announced capability: {} v{}", agentId, capability, version);
}
/**
* Logs a message with the agent ID as context.
*
* @param message the message to log
*/
protected final void log(String message) {
logger.info("[{}] {}", agentId, message);
}
/**
* Logs a message with arguments and the agent ID as context.
*
* @param message the message template
* @param args the message arguments
*/
protected final void log(String message, Object... args) {
logger.info("[{}] " + message, agentId, args);
}
/**
* Gets the agent's metadata.
*
* @return a copy of the metadata map
*/
public final Map<String, Object> getMetadata() {
return new HashMap<>(metadata);
}
/**
* Sets a metadata value.
*
* @param key the metadata key
* @param value the metadata value
*/
protected final void setMetadata(String key, Object value) {
metadata.put(key, value);
}
/**
* Gets the agent's subscriptions.
*
* @return a copy of the subscriptions list
*/
public final List<String> getSubscriptions() {
return new ArrayList<>(subscriptions);
}
/**
* Gets the agent's capabilities.
*
* @return a copy of the capabilities map
*/
public final Map<String, String> getCapabilities() {
return new HashMap<>(capabilities);
}
/**
* Gets the time when this agent was activated.
*
* @return the activation time, or null if not activated
*/
public final Instant getActivationTime() {
return activationTime;
}
/**
* Gets the time of the last activity (event published).
*
* @return the last activity time
*/
public final Instant getLastActivityTime() {
return lastActivityTime;
}
/**
* Handles an incoming event. Called by the AMCP framework.
*
* @param event the event to handle
*/
public final void handleEvent(Event event) {
if (state != AgentState.ACTIVE) {
logger.warn("Agent {} received event while not active: {}", agentId, state);
return;
}
lastActivityTime = Instant.now();
try {
onEventReceived(event);
} catch (Exception e) {
logger.error("Error handling event in agent {}: {}", agentId, e.getMessage(), e);
}
}
/**
* Called when an event is received by this agent.
*
* Subclasses should override this method to handle incoming events.
*
* @param event the received event
*/
protected void onEventReceived(Event event) {
// Default implementation does nothing
}
/**
* Convenience method to get agent ID (alias for getAgentId).
*
* @return the agent ID
*/
public final String getId() {
return agentId;
}
/**
* Convenience method to get agent type (alias for getAgentType).
*
* @return the agent type
*/
public final String getType() {
return agentType;
}
@Override
public String toString() {
return String.format("Agent{id='%s', type='%s', state=%s}",
agentId, agentType, state);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Agent agent = (Agent) obj;
return Objects.equals(agentId, agent.agentId);
}
@Override
public int hashCode() {
return Objects.hash(agentId);
}
}
/**
* Enumeration of agent states.
*/
enum AgentState {
CREATED,
ACTIVATING,
ACTIVE,
DEACTIVATING,
DEACTIVATED,
ERROR
}
/**
* Exception thrown when agent activation fails.
*/
class AgentActivationException extends RuntimeException {
public AgentActivationException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Exception thrown when agent deactivation fails.
*/
class AgentDeactivationException extends RuntimeException {
public AgentDeactivationException(String message, Throwable cause) {
super(message, cause);
}
}