CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-starter

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.

Overview
Eval results
Files

guides-defining-actions.mddocs/

Defining Actions

Step-by-step guide for defining actions with preconditions, postconditions, cost, and value for GOAP planning.

1. Basic Action Definition

Java

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);
    }
}

Kotlin

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)
    }
}

2. Actions with Preconditions and Postconditions

Preconditions define what must be true before an action can execute. Postconditions define what will be true after execution.

Java

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);
    }
}

Kotlin

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)
    }
}

3. Actions with Dynamic Cost and Value

Use @Cost methods to compute cost/value dynamically based on runtime state.

Java

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;
    }
}

Kotlin

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
    }
}

4. Actions with Output Binding

Use outputBinding to store action results in the blackboard for use by subsequent actions.

Java

@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);
    }
}

Kotlin

@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)
    }
}

5. Actions with Clear Blackboard

Use clearBlackboard = true to clear the blackboard after action execution.

Java

@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
    }
}

Kotlin

@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
    }
}

6. Actions with canRerun Control

Use canRerun = false for actions that should only execute once per planning cycle.

Java

@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);
    }
}

Kotlin

@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)
    }
}

7. Actions with Tool Groups

Actions can require and provide tool groups for LLM operations.

Java

@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);
    }
}

Kotlin

@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")
    }
}

8. Actions with Triggers

Use triggers to automatically invoke actions when specific types appear in the blackboard.

Java

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);
    }
}

Kotlin

@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)
    }
}

9. Actions with @Provided Parameters

Use @Provided to inject platform services that don't participate in planning.

Java

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;
    }
}

Kotlin

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
    }
}

10. Complete Working Example

Java - Multi-Stage ETL Agent

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();
    }
}

Kotlin - Machine Learning Pipeline

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"
            )
        }
    }
}

Key Annotation Attributes

@Action

  • description (required) - Human-readable description
  • pre - Preconditions that must be true (array of strings)
  • post - Postconditions that will be true (array of strings)
  • canRerun - Whether action can execute multiple times (default: true)
  • clearBlackboard - Clear blackboard after execution (default: false)
  • outputBinding - Variable name to bind result in blackboard (default: empty)
  • cost - Static cost of execution (default: 0.0)
  • value - Static value/benefit (default: 0.0)
  • costMethod - Name of @Cost method for dynamic cost (default: empty)
  • valueMethod - Name of @Cost method for dynamic value (default: empty)
  • toolGroups - Tool groups this action provides (default: empty)
  • toolGroupRequirements - Tool groups this action requires (default: empty)
  • trigger - Type that triggers this action (default: Nothing)
  • actionRetryPolicy - Retry strategy (default: DEFAULT)

@Cost

  • name (required) - Unique name for cost/value computation method

@Provided

  • Marks parameters as platform-provided (no attributes)

Best Practices

  1. Clear Descriptions - Write precise action descriptions for planning
  2. Use Preconditions/Postconditions - Define clear state requirements for GOAP
  3. Bind Outputs - Use outputBinding to pass data between actions
  4. Set Appropriate Costs - Balance cost vs value for optimal planning
  5. Use Dynamic Costs - Compute costs based on runtime state when appropriate
  6. Control Reruns - Set canRerun=false for idempotent operations
  7. Inject Platform Services - Use @Provided for Ai, ActionContext, OutputChannel
  8. Track Progress - Update progress in long-running actions
  9. Handle Retries - Configure appropriate retry policies for unreliable operations
  10. Use Tool Groups - Organize tools by functionality and declare requirements

See Also

  • Creating Agents - Create agents with planning
  • Creating Tools - Expose LLM tools
  • Goal Achievement - Define goal-achieving actions
  • Human-in-the-Loop - Implement HITL patterns
tessl i tessl/maven-com-embabel-agent--embabel-agent-starter@0.3.1

docs

api-annotations.md

api-domain-model.md

api-invocation.md

api-tools.md

concepts-actions.md

concepts-agents.md

concepts-goals.md

concepts-invocation.md

concepts-tools.md

guides-creating-agents.md

guides-creating-tools.md

guides-defining-actions.md

guides-goal-achievement.md

guides-human-in-loop.md

guides-multimodal.md

index.md

integration-mcp.md

integration-model-providers.md

integration-spring-boot.md

LlmTool.md

quickstart.md

reference-component-scanning.md

reference-configuration-properties.md

reference-installation.md

reference-logging.md

reference-resilience.md

reference-streaming.md

tile.json