Base starter module for the Embabel Agent Framework providing core dependencies for building agentic flows on the JVM with Spring Boot integration and GOAP-based intelligent path finding.
Step-by-step guide for defining actions with preconditions, postconditions, cost, and value for GOAP planning.
import com.embabel.agent.api.annotation.Action;
import com.embabel.agent.api.annotation.Agent;
@Agent(description = "Simple agent with actions")
public class SimpleAgent {
@Action(description = "Fetch data from source")
public Data fetchData(String source) {
return dataService.fetch(source);
}
@Action(description = "Process the fetched data")
public ProcessedData processData(Data data) {
return processor.process(data);
}
}import com.embabel.agent.api.annotation.Action
import com.embabel.agent.api.annotation.Agent
@Agent(description = "Simple Kotlin agent")
class SimpleAgent {
@Action(description = "Load data from database")
fun loadData(query: String): Dataset {
return database.query(query)
}
@Action(description = "Transform loaded data")
fun transformData(dataset: Dataset): TransformedData {
return transformer.transform(dataset)
}
}Preconditions define what must be true before an action can execute. Postconditions define what will be true after execution.
import com.embabel.agent.api.annotation.Action;
@Agent(description = "Task processor")
public class TaskAgent {
@Action(
description = "Fetch task details from database",
post = {"taskLoaded"},
outputBinding = "task",
cost = 5.0,
value = 10.0
)
public Task fetchTask(String taskId) {
return taskRepository.findById(taskId);
}
@Action(
description = "Validate the loaded task",
pre = {"taskLoaded"},
post = {"taskValidated"},
cost = 2.0,
value = 15.0
)
public ValidationResult validateTask(Task task) {
return validator.validate(task);
}
@Action(
description = "Process the validated task",
pre = {"taskValidated"},
post = {"taskProcessed"},
outputBinding = "result",
cost = 20.0,
value = 50.0
)
public ProcessedResult processTask(Task task, @Provided Ai ai) {
return ai.createObject("Process: " + task);
}
@Action(
description = "Save processed result to database",
pre = {"taskProcessed"},
post = {"taskSaved"},
canRerun = false,
cost = 5.0,
value = 30.0
)
public void saveResult(ProcessedResult result) {
resultRepository.save(result);
}
}import com.embabel.agent.api.annotation.Action
@Agent(description = "Order processor")
class OrderAgent {
@Action(
description = "Load order from database",
post = ["orderLoaded"],
outputBinding = "order",
cost = 3.0,
value = 10.0
)
fun loadOrder(orderId: String): Order {
return orderRepository.findById(orderId)
}
@Action(
description = "Validate order data",
pre = ["orderLoaded"],
post = ["orderValidated"],
cost = 2.0,
value = 15.0
)
fun validateOrder(order: Order): ValidationResult {
return validator.validate(order)
}
@Action(
description = "Calculate shipping cost",
pre = ["orderValidated"],
post = ["shippingCalculated"],
outputBinding = "shippingCost",
cost = 5.0,
value = 20.0
)
fun calculateShipping(order: Order): Double {
return shippingCalculator.calculate(order)
}
@Action(
description = "Finalize order with shipping",
pre = ["orderValidated", "shippingCalculated"],
post = ["orderFinalized"],
canRerun = false,
cost = 8.0,
value = 40.0
)
fun finalizeOrder(order: Order, shippingCost: Double): FinalizedOrder {
return finalizer.finalize(order, shippingCost)
}
}Use @Cost methods to compute cost/value dynamically based on runtime state.
import com.embabel.agent.api.annotation.Cost;
import com.embabel.agent.api.annotation.Action;
@Agent(description = "Resource allocator")
public class ResourceAgent {
@Action(
description = "Allocate compute resources",
costMethod = "computeAllocationCost",
valueMethod = "computeAllocationValue"
)
public Allocation allocateResources(Request request) {
return allocator.allocate(request);
}
@Cost(name = "computeAllocationCost")
public double computeAllocationCost(Request request) {
// Dynamic cost based on current system load
double systemLoad = monitor.getCurrentLoad();
return request.getResourceCount() * systemLoad * 10.0;
}
@Cost(name = "computeAllocationValue")
public double computeAllocationValue(Request request) {
// Value based on request priority
return request.getPriority() * 100.0;
}
}import com.embabel.agent.api.annotation.Cost
import com.embabel.agent.api.annotation.Action
@Agent(description = "Cache manager")
class CacheAgent {
@Action(
description = "Cache data item",
costMethod = "cachingCost",
valueMethod = "cachingValue"
)
fun cacheItem(key: String, value: Any): Unit {
cache.put(key, value)
}
@Cost(name = "cachingCost")
fun cachingCost(key: String, value: Any): Double {
val size = estimateSize(value)
val availableMemory = cache.getAvailableMemory()
return if (size > availableMemory) 100.0 else 1.0
}
@Cost(name = "cachingValue")
fun cachingValue(key: String, value: Any): Double {
val accessFrequency = cache.getAccessFrequency(key)
return accessFrequency * 50.0
}
}Use outputBinding to store action results in the blackboard for use by subsequent actions.
@Agent(description = "Data pipeline")
public class PipelineAgent {
@Action(
description = "Extract data from source",
post = {"dataExtracted"},
outputBinding = "rawData",
cost = 10.0
)
public RawData extractData(String source) {
return extractor.extract(source);
}
@Action(
description = "Transform raw data",
pre = {"dataExtracted"},
post = {"dataTransformed"},
outputBinding = "transformedData",
cost = 15.0
)
public TransformedData transformData(RawData rawData) {
// rawData is retrieved from blackboard
return transformer.transform(rawData);
}
@Action(
description = "Load transformed data",
pre = {"dataTransformed"},
post = {"dataLoaded"},
cost = 10.0
)
public void loadData(TransformedData transformedData) {
// transformedData is retrieved from blackboard
loader.load(transformedData);
}
}@Agent(description = "Processing pipeline")
class ProcessingAgent {
@Action(
description = "Read input file",
post = ["fileRead"],
outputBinding = "fileContent",
cost = 5.0
)
fun readFile(path: String): String {
return Files.readString(Paths.get(path))
}
@Action(
description = "Parse file content",
pre = ["fileRead"],
post = ["contentParsed"],
outputBinding = "parsedData",
cost = 10.0
)
fun parseContent(fileContent: String): ParsedData {
return parser.parse(fileContent)
}
@Action(
description = "Process parsed data",
pre = ["contentParsed"],
post = ["dataProcessed"],
outputBinding = "result",
cost = 20.0
)
fun processData(parsedData: ParsedData): Result {
return processor.process(parsedData)
}
}Use clearBlackboard = true to clear the blackboard after action execution.
@Agent(description = "Batch processor")
public class BatchAgent {
@Action(
description = "Process batch item",
post = {"itemProcessed"},
outputBinding = "processedItem",
clearBlackboard = false,
cost = 10.0
)
public ProcessedItem processBatchItem(Item item) {
return processor.process(item);
}
@Action(
description = "Finalize batch",
pre = {"itemProcessed"},
clearBlackboard = true, // Clear for next batch
cost = 5.0
)
public void finalizeBatch(ProcessedItem processedItem) {
finalizer.finalize(processedItem);
// Blackboard is cleared after this action
}
}@Agent(description = "Session manager")
class SessionAgent {
@Action(
description = "Handle user session",
post = ["sessionActive"],
outputBinding = "session",
clearBlackboard = false
)
fun handleSession(userId: String): Session {
return sessionManager.create(userId)
}
@Action(
description = "Close session",
pre = ["sessionActive"],
clearBlackboard = true // Clear session data
)
fun closeSession(session: Session) {
sessionManager.close(session)
// Blackboard cleared for next session
}
}Use canRerun = false for actions that should only execute once per planning cycle.
@Agent(description = "Report generator")
public class ReportAgent {
@Action(
description = "Gather report data",
post = {"dataGathered"},
outputBinding = "reportData",
canRerun = true, // Can be called multiple times
cost = 10.0
)
public ReportData gatherData(String query) {
return dataSource.query(query);
}
@Action(
description = "Generate report from data",
pre = {"dataGathered"},
post = {"reportGenerated"},
outputBinding = "report",
canRerun = false, // Should only run once
cost = 20.0
)
public Report generateReport(ReportData reportData) {
return generator.generate(reportData);
}
@Action(
description = "Send report to recipients",
pre = {"reportGenerated"},
canRerun = false, // Should only send once
cost = 5.0
)
public void sendReport(Report report) {
emailService.send(report);
}
}@Agent(description = "Notification sender")
class NotificationAgent {
@Action(
description = "Prepare notification content",
post = ["contentPrepared"],
outputBinding = "notification",
canRerun = true
)
fun prepareNotification(template: String, data: Map<String, Any>): Notification {
return notificationBuilder.build(template, data)
}
@Action(
description = "Send notification to users",
pre = ["contentPrepared"],
canRerun = false // Only send once
)
fun sendNotification(notification: Notification) {
notificationService.send(notification)
}
}Actions can require and provide tool groups for LLM operations.
@Agent(description = "Analysis agent")
public class AnalysisAgent {
@Action(
description = "Analyze data with LLM tools",
pre = {"dataLoaded"},
post = {"dataAnalyzed"},
toolGroupRequirements = {"statistical-analysis", "data-visualization"},
cost = 30.0,
value = 60.0
)
public AnalysisReport analyzeData(Dataset data, @Provided Ai ai) {
// This action requires statistical-analysis and data-visualization tools
return ai.withLlm(OpenAiModels.GPT_4_TURBO)
.createObject("Analyze this dataset: " + data);
}
@Action(
description = "Generate summary report",
pre = {"dataAnalyzed"},
post = {"reportGenerated"},
toolGroups = {"report-tools"}, // Provides report-tools
cost = 15.0,
value = 40.0
)
public Report generateReport(AnalysisReport analysis) {
return reportGenerator.generate(analysis);
}
}@Agent(description = "Data processor with tools")
class DataToolAgent {
@Action(
description = "Process with specialized tools",
toolGroupRequirements = ["data-cleaning", "data-enrichment"],
cost = 25.0,
value = 50.0
)
fun processWithTools(input: RawData, @Provided ai: Ai): ProcessedData {
return ai.withLlm(GeminiModels.GEMINI_2_5_PRO)
.createObject("Process: $input")
}
}Use triggers to automatically invoke actions when specific types appear in the blackboard.
import com.embabel.agent.api.annotation.Action;
@Agent(description = "Event handler")
public class EventAgent {
@Action(
description = "Handle user event",
trigger = UserEvent.class,
post = {"userEventHandled"}
)
public void handleUserEvent(UserEvent event) {
// Automatically triggered when UserEvent appears in blackboard
eventProcessor.process(event);
}
@Action(
description = "Handle system event",
trigger = SystemEvent.class,
post = {"systemEventHandled"}
)
public void handleSystemEvent(SystemEvent event) {
// Automatically triggered when SystemEvent appears in blackboard
systemProcessor.process(event);
}
}@Agent(description = "Message handler")
class MessageAgent {
@Action(
description = "Process incoming message",
trigger = IncomingMessage::class,
post = ["messageProcessed"]
)
fun processMessage(message: IncomingMessage) {
messageProcessor.process(message)
}
@Action(
description = "Handle error event",
trigger = ErrorEvent::class,
post = ["errorHandled"]
)
fun handleError(error: ErrorEvent) {
errorHandler.handle(error)
}
}Use @Provided to inject platform services that don't participate in planning.
import com.embabel.agent.api.annotation.Provided;
import com.embabel.agent.api.llm.Ai;
import com.embabel.agent.api.ActionContext;
@Agent(description = "LLM processing agent")
public class LlmAgent {
@Action(description = "Generate content with LLM")
public Article generateContent(
String topic,
@Provided Ai ai,
@Provided ActionContext context
) {
// ai and context are provided by platform
context.updateProgress("Generating content for: " + topic);
return ai.withLlm(OpenAiModels.GPT_4_TURBO)
.createObject("Write an article about: " + topic);
}
@Action(description = "Analyze data with progress tracking")
public Analysis analyzeData(
Dataset data,
@Provided Ai ai,
@Provided ActionContext context,
@Provided OutputChannel channel
) {
context.updateProgress("Starting analysis");
channel.send(new OutputChannelEvent(
EventType.PROGRESS,
Map.of("stage", "initialization")
));
Analysis result = ai.createObject("Analyze: " + data);
context.updateProgress("Analysis complete");
return result;
}
}import com.embabel.agent.api.annotation.Provided
import com.embabel.agent.api.llm.Ai
import com.embabel.agent.api.ActionContext
@Agent(description = "AI-powered agent")
class AiAgent {
@Action(description = "Process with AI assistance")
fun processWithAi(
input: Input,
@Provided ai: Ai,
@Provided context: ActionContext
): Output {
context.updateProgress("Processing ${input.size} items")
context.sendMessage(Message.info("Starting AI processing"))
val result = ai.withLlm(GeminiModels.GEMINI_2_5_PRO)
.createObject("Process: $input")
context.sendMessage(Message.info("Processing complete"))
return result
}
}import com.embabel.agent.api.annotation.*;
import com.embabel.agent.api.ActionRetryPolicy;
@Agent(
description = "ETL pipeline agent with full GOAP planning",
planner = PlannerType.GOAP
)
public class EtlAgent {
private final DataSource dataSource;
private final DataValidator validator;
private final DataTransformer transformer;
private final DataLoader loader;
public EtlAgent(
DataSource dataSource,
DataValidator validator,
DataTransformer transformer,
DataLoader loader
) {
this.dataSource = dataSource;
this.validator = validator;
this.transformer = transformer;
this.loader = loader;
}
@Action(
description = "Extract data from source",
post = {"dataExtracted"},
outputBinding = "rawData",
cost = 15.0,
value = 20.0,
actionRetryPolicy = ActionRetryPolicy.exponential(3, 1000)
)
public RawData extractData(
String source,
@Provided ActionContext context
) {
context.updateProgress("Extracting data from: " + source);
return dataSource.extract(source);
}
@Action(
description = "Validate extracted data",
pre = {"dataExtracted"},
post = {"dataValidated"},
outputBinding = "validatedData",
cost = 5.0,
value = 25.0
)
public ValidatedData validateData(
RawData rawData,
@Provided ActionContext context
) {
context.updateProgress("Validating data");
ValidationResult result = validator.validate(rawData);
if (!result.isValid()) {
throw new ValidationException(result.getErrors());
}
return new ValidatedData(rawData);
}
@Action(
description = "Transform validated data",
pre = {"dataValidated"},
post = {"dataTransformed"},
outputBinding = "transformedData",
costMethod = "computeTransformCost",
valueMethod = "computeTransformValue"
)
public TransformedData transformData(
ValidatedData validatedData,
@Provided Ai ai,
@Provided ActionContext context
) {
context.updateProgress("Transforming data");
context.sendMessage(Message.info(
"Processing " + validatedData.getRecordCount() + " records"
));
return transformer.transform(validatedData);
}
@Cost(name = "computeTransformCost")
public double computeTransformCost(ValidatedData data) {
// Cost increases with data size
return data.getRecordCount() * 0.01;
}
@Cost(name = "computeTransformValue")
public double computeTransformValue(ValidatedData data) {
// Value based on data quality
return data.getQualityScore() * 50.0;
}
@Action(
description = "Load transformed data to destination",
pre = {"dataTransformed"},
post = {"dataLoaded"},
canRerun = false,
cost = 20.0,
value = 50.0,
actionRetryPolicy = ActionRetryPolicy.exponential(5, 2000)
)
public LoadResult loadData(
TransformedData transformedData,
@Provided ActionContext context
) {
context.updateProgress("Loading data to destination");
LoadResult result = loader.load(transformedData);
context.sendMessage(Message.info(
"Loaded " + result.getRecordCount() + " records"
));
return result;
}
@Action(
description = "Cleanup temporary resources",
pre = {"dataLoaded"},
clearBlackboard = true,
cost = 2.0,
value = 10.0
)
public void cleanup(@Provided ActionContext context) {
context.updateProgress("Cleaning up");
dataSource.cleanup();
transformer.cleanup();
loader.cleanup();
}
}import com.embabel.agent.api.annotation.*
import com.embabel.agent.api.ActionRetryPolicy
@Agent(
description = "ML training pipeline with GOAP",
planner = PlannerType.GOAP
)
class MlPipelineAgent(
private val dataLoader: DataLoader,
private val featureEngineering: FeatureEngineering,
private val modelTrainer: ModelTrainer,
private val modelEvaluator: ModelEvaluator
) {
@Action(
description = "Load training data",
post = ["trainingDataLoaded"],
outputBinding = "trainingData",
cost = 10.0,
value = 20.0,
actionRetryPolicy = ActionRetryPolicy.fixed(3, 2000)
)
fun loadTrainingData(
datasetPath: String,
@Provided context: ActionContext
): TrainingData {
context.updateProgress("Loading training data from $datasetPath")
return dataLoader.load(datasetPath)
}
@Action(
description = "Engineer features from training data",
pre = ["trainingDataLoaded"],
post = ["featuresEngineered"],
outputBinding = "features",
cost = 20.0,
value = 40.0
)
fun engineerFeatures(
trainingData: TrainingData,
@Provided ai: Ai,
@Provided context: ActionContext
): Features {
context.updateProgress("Engineering features")
context.sendMessage(Message.info(
"Processing ${trainingData.size} samples"
))
return featureEngineering.engineer(trainingData)
}
@Action(
description = "Train machine learning model",
pre = ["featuresEngineered"],
post = ["modelTrained"],
outputBinding = "model",
costMethod = "trainingCost",
valueMethod = "trainingValue"
)
fun trainModel(
features: Features,
@Provided context: ActionContext
): TrainedModel {
context.updateProgress("Training model")
context.sendMessage(Message.progress("Training", 0.0))
val model = modelTrainer.train(features) { progress ->
context.sendMessage(Message.progress("Training", progress))
}
context.sendMessage(Message.progress("Training", 100.0))
return model
}
@Cost(name = "trainingCost")
fun trainingCost(features: Features): Double {
return features.dimensionality * features.sampleCount * 0.001
}
@Cost(name = "trainingValue")
fun trainingValue(features: Features): Double {
return features.qualityScore * 100.0
}
@Action(
description = "Evaluate trained model",
pre = ["modelTrained"],
post = ["modelEvaluated"],
outputBinding = "evaluation",
canRerun = false,
cost = 15.0,
value = 50.0
)
fun evaluateModel(
model: TrainedModel,
trainingData: TrainingData,
@Provided context: ActionContext
): Evaluation {
context.updateProgress("Evaluating model")
val evaluation = modelEvaluator.evaluate(model, trainingData)
context.sendMessage(Message.info(
"Accuracy: ${evaluation.accuracy}, F1: ${evaluation.f1Score}"
))
return evaluation
}
@Action(
description = "Save model if evaluation passes threshold",
pre = ["modelEvaluated"],
post = ["modelSaved"],
canRerun = false,
cost = 5.0,
value = 30.0
)
fun saveModel(
model: TrainedModel,
evaluation: Evaluation,
@Provided context: ActionContext
) {
if (evaluation.accuracy >= 0.85) {
context.updateProgress("Saving model")
modelTrainer.save(model)
context.sendMessage(Message.info("Model saved successfully"))
} else {
throw ModelQualityException(
"Model accuracy ${evaluation.accuracy} below threshold 0.85"
)
}
}
}tessl i tessl/maven-com-embabel-agent--embabel-agent-starter@0.3.1docs