CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-api

Fluent DSL and Kotlin DSL for building autonomous agents with planning capabilities on the JVM, featuring annotation-based and programmatic configuration for agentic flows with Spring Boot integration

Overview
Eval results
Files

actions-goals.mddocs/

Actions and Goals

Actions are the executable steps in an agent's workflow. Goals define desired outcomes that drive the planning process. The framework uses GOAP (Goal-Oriented Action Planning) to find optimal action sequences to achieve goals.

Capabilities

Action Definition

Define actions that transform state and produce outputs.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Action(
    /** Human-readable description of what the action does */
    val description: String = "",
    /** Preconditions that must be true before action can execute */
    val pre: Array<String> = [],
    /** Postconditions that become true after action executes */
    val post: Array<String> = [],
    /** Whether action can be executed multiple times in a plan */
    val canRerun: Boolean = false,
    /** Whether to clear blackboard before execution */
    val clearBlackboard: Boolean = false,
    /** Name to bind output to on blackboard */
    val outputBinding: String = IoBinding.DEFAULT_BINDING,
    /** Static cost (0.0-1.0) for planning */
    val cost: ZeroToOne = 0.0,
    /** Value (0.0-1.0) for utility-based planning */
    val value: ZeroToOne = 0.0,
    /** Name of method providing dynamic cost */
    val costMethod: String = "",
    /** Name of method providing dynamic value */
    val valueMethod: String = "",
    /** Tool groups required by this action */
    @Deprecated("Add tools to individual LLM calls instead")
    val toolGroups: Array<String> = [],
    /** Tool group requirements */
    @Deprecated("Add tools to individual LLM calls instead")
    val toolGroupRequirements: Array<ToolGroupRequirement> = [],
    /** Trigger expression for action */
    val trigger: KClass<*> = Unit::class,
    /** Retry policy for failed actions */
    val actionRetryPolicy: ActionRetryPolicy = ActionRetryPolicy.DEFAULT,
    /** SpEL expression for retry policy */
    val actionRetryPolicyExpression: String = ""
)

Basic Action Example:

@Agent(name = "user-manager", provider = "auth", description = "Manages user accounts")
class UserManagerAgent {

    @Action(description = "Create new user account")
    fun createUser(
        username: String,
        email: String,
        password: String
    ): User {
        val hashedPassword = passwordService.hash(password)
        return userRepository.create(username, email, hashedPassword)
    }

    @Action(
        description = "Send welcome email to new user",
        pre = ["user_created"]
    )
    fun sendWelcomeEmail(user: User): EmailReceipt {
        val emailBody = templateEngine.render("welcome", user)
        return emailService.send(user.email, "Welcome!", emailBody)
    }
}

Action with Context Access:

@Agent(name = "report-generator", provider = "analytics", description = "Generates reports")
class ReportGeneratorAgent {

    @Action(description = "Generate insights from data using LLM")
    fun generateInsights(data: DataSet, context: ActionContext): Insights {
        // Access LLM through context
        val insights = context.promptRunner()
            .creating(Insights::class.java)
            .withExample("Revenue increased by 15%", Insights("revenue", 0.15))
            .fromPrompt("""
                Analyze this data and extract key insights:
                ${data.toSummary()}
            """)

        // Update progress
        context.updateProgress("Generated ${insights.items.size} insights")

        return insights
    }

    @Action(
        description = "Format report as PDF",
        pre = ["insights_generated"],
        post = ["report_formatted"]
    )
    fun formatReport(insights: Insights): Report {
        return pdfFormatter.format(insights)
    }
}

Action with Preconditions and Postconditions:

@Agent(name = "payment-processor", provider = "finance", description = "Processes payments")
class PaymentProcessorAgent {

    @Action(
        description = "Validate payment information",
        post = ["payment_validated"]
    )
    fun validatePayment(payment: PaymentInfo): ValidationResult {
        return paymentValidator.validate(payment)
    }

    @Action(
        description = "Process payment transaction",
        pre = ["payment_validated"],
        post = ["payment_processed"],
        cost = 0.3  // Higher cost action
    )
    fun processPayment(payment: PaymentInfo): TransactionResult {
        return paymentGateway.process(payment)
    }

    @Action(
        description = "Send payment receipt",
        pre = ["payment_processed"],
        cost = 0.1  // Lower cost action
    )
    fun sendReceipt(transaction: TransactionResult): EmailReceipt {
        return emailService.sendReceipt(transaction)
    }
}

Goal Achievement

Define goals that actions achieve. Goals drive the GOAP planning algorithm.

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AchievesGoal {
    /** Description of the goal being achieved */
    String description();
    /** Value of achieving this goal (0.0-1.0) */
    double value() default 0.0;
    /** Tags for categorizing goals */
    String[] tags() default {};
    /** Example prompts for goal */
    String[] examples() default {};
    /** Export configuration */
    Export export() default @Export();
}

@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Export {
    String name() default "";
    boolean remote() default false;
    boolean local() default true;
    Class<?>[] startingInputTypes() default {};
}

Goal Achievement Example:

@Agent(name = "customer-support", provider = "support", description = "Provides customer support")
class CustomerSupportAgent {

    @Action(description = "Resolve customer issue")
    @AchievesGoal(
        description = "Customer issue has been resolved",
        value = 1.0,
        tags = ["support", "resolution"]
    )
    fun resolveIssue(issue: Issue, solution: Solution): Resolution {
        return issueTracker.resolve(issue, solution)
    }

    @Action(description = "Escalate to human agent")
    @AchievesGoal(
        description = "Issue escalated to human support",
        value = 0.5,
        tags = ["support", "escalation"]
    )
    fun escalateToHuman(issue: Issue): Escalation {
        return supportQueue.escalate(issue)
    }
}

Dynamic Cost and Value Calculation

The @Cost annotation allows you to define methods that compute dynamic action costs and values at planning time. This enables the GOAP planner to make intelligent decisions based on runtime state and context.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Cost(
    /** Name for the cost function (defaults to method name) */
    val name: String = ""
)

Key Concepts:

  • Cost Methods: Calculate the computational or resource cost of executing an action (typically 0.0-1.0, where lower is better)
  • Value Methods: Calculate the utility or benefit of executing an action (typically 0.0-1.0, where higher is better)
  • Planning Time Evaluation: Cost and value methods are called during planning, before action execution
  • Nullable Parameters: CRITICAL REQUIREMENT - All parameters in cost/value methods must be nullable because values may not exist during planning
  • Return Type: Methods must return Double
  • Integration: Connect to actions via @Action(costMethod = "methodName") or @Action(valueMethod = "methodName")

Basic Cost Method Example:

@Agent(name = "data-fetcher", provider = "api", description = "Fetches data from APIs")
class DataFetcherAgent {

    @Action(
        description = "Fetch data from external API",
        costMethod = "apiCallCost"  // References the cost method
    )
    fun fetchFromApi(endpoint: String, context: ActionContext): ApiResponse {
        return apiClient.get(endpoint)
    }

    @Cost(name = "apiCallCost")
    fun calculateApiCost(endpoint: String?): Double {
        // Parameters MUST be nullable - planning time evaluation
        return when (endpoint) {
            null -> 0.5  // Default cost when value unknown
            "premium" -> 0.8  // Premium endpoints are expensive
            "standard" -> 0.3  // Standard endpoints are cheaper
            else -> 0.5
        }
    }

    @Action(
        description = "Fetch data from cache",
        cost = 0.1  // Static low cost (no cost method needed)
    )
    fun fetchFromCache(key: String): CachedData? {
        return cache.get(key)
    }
}

Value Method for Priority Scoring:

@Agent(name = "task-scheduler", provider = "automation", description = "Schedules tasks")
class TaskSchedulerAgent {

    @Action(
        description = "Execute high-priority task",
        valueMethod = "taskPriorityValue",
        costMethod = "taskExecutionCost"
    )
    fun executeTask(task: Task?): TaskResult {
        return taskExecutor.execute(task!!)
    }

    @Cost(name = "taskPriorityValue")
    fun calculateTaskValue(task: Task?): Double {
        // Higher value = higher priority
        return when (task?.priority) {
            null -> 0.5
            "CRITICAL" -> 1.0
            "HIGH" -> 0.8
            "MEDIUM" -> 0.5
            "LOW" -> 0.3
            else -> 0.4
        }
    }

    @Cost(name = "taskExecutionCost")
    fun calculateTaskCost(task: Task?): Double {
        // Cost based on complexity
        val complexity = task?.complexity ?: return 0.5
        return when {
            complexity > 100 -> 0.9
            complexity > 50 -> 0.6
            else -> 0.3
        }
    }
}

Accessing Blackboard State in Cost Methods:

Cost and value methods can access blackboard state to make decisions based on current context.

@Agent(name = "resource-manager", provider = "system", description = "Manages system resources")
class ResourceManagerAgent {

    @Action(
        description = "Process batch job",
        costMethod = "batchProcessingCost"
    )
    fun processBatch(
        batchSize: Int,
        @Provided blackboard: Blackboard
    ): BatchResult {
        return batchProcessor.process(batchSize)
    }

    @Cost(name = "batchProcessingCost")
    fun calculateBatchCost(
        batchSize: Int?,
        @Provided blackboard: Blackboard?
    ): Double {
        // Access blackboard state during planning
        val systemLoad = blackboard?.get("system_load", Double::class.java) ?: 0.5
        val size = batchSize ?: 100

        // Cost increases with batch size and system load
        val baseCost = size / 1000.0
        val loadMultiplier = 1.0 + systemLoad

        return (baseCost * loadMultiplier).coerceIn(0.0, 1.0)
    }

    @Action(
        description = "Process single item",
        cost = 0.1
    )
    fun processSingleItem(item: Item): ItemResult {
        return itemProcessor.process(item)
    }
}

Dynamic Cost Based on Context:

@Agent(name = "weather-service", provider = "api", description = "Provides weather data")
class WeatherServiceAgent {

    @Action(
        description = "Get weather from external API",
        costMethod = "weatherApiCost",
        valueMethod = "weatherDataValue"
    )
    fun getWeatherFromApi(
        location: String,
        @Provided blackboard: Blackboard
    ): Weather {
        return weatherApi.getWeather(location)
    }

    @Cost(name = "weatherApiCost")
    fun calculateApiCost(
        location: String?,
        @Provided blackboard: Blackboard?
    ): Double {
        // Check if we have cached data
        val cacheAge = blackboard?.get("weather_cache_age_minutes", Int::class.java)

        // If cache is fresh (< 30 minutes), API call has high cost
        return if (cacheAge != null && cacheAge < 30) {
            0.9  // Expensive - we have recent data
        } else {
            0.4  // Reasonable - data is stale
        }
    }

    @Cost(name = "weatherDataValue")
    fun calculateDataValue(
        location: String?,
        @Provided blackboard: Blackboard?
    ): Double {
        // Value decreases if we already have recent data
        val cacheAge = blackboard?.get("weather_cache_age_minutes", Int::class.java)

        return when {
            cacheAge == null -> 1.0  // No data, high value
            cacheAge < 30 -> 0.2     // Fresh data, low value
            cacheAge < 120 -> 0.6    // Aging data, medium value
            else -> 0.9              // Stale data, high value
        }
    }

    @Action(
        description = "Get weather from cache",
        cost = 0.1,
        value = 0.5
    )
    fun getWeatherFromCache(location: String): Weather? {
        return weatherCache.get(location)
    }
}

How GOAP Planner Uses Cost and Value:

The GOAP planner uses cost and value to select optimal action sequences:

  1. Cost: Lower cost actions are preferred when multiple paths achieve the same goal
  2. Value: Higher value actions are preferred when optimizing for utility
  3. Combined Scoring: Planner balances value - cost to find optimal plans
  4. Dynamic Evaluation: Cost and value methods are called during planning with current blackboard state

Complete Agent Example with Cost and Value Methods:

@Agent(
    name = "smart-delivery",
    provider = "logistics",
    description = "Optimizes delivery routes and methods"
)
class SmartDeliveryAgent(
    private val deliveryService: DeliveryService,
    private val routeOptimizer: RouteOptimizer,
    private val trafficService: TrafficService
) {

    @Action(
        description = "Calculate optimal delivery route",
        post = ["route_calculated"],
        costMethod = "routeCalculationCost",
        valueMethod = "routeOptimizationValue"
    )
    fun calculateRoute(
        destination: String,
        @Provided blackboard: Blackboard
    ): Route {
        val traffic = trafficService.getCurrentTraffic()
        blackboard.put("current_traffic", traffic)
        return routeOptimizer.optimize(destination, traffic)
    }

    @Cost(name = "routeCalculationCost")
    fun calculateRouteCost(
        destination: String?,
        @Provided blackboard: Blackboard?
    ): Double {
        // Cost based on whether we need live traffic data
        val hasRecentTraffic = blackboard?.contains("current_traffic") ?: false
        return if (hasRecentTraffic) 0.3 else 0.7
    }

    @Cost(name = "routeOptimizationValue")
    fun calculateRouteValue(
        destination: String?,
        @Provided blackboard: Blackboard?
    ): Double {
        // Higher value for urgent deliveries
        val priority = blackboard?.get("delivery_priority", String::class.java)
        return when (priority) {
            "URGENT" -> 1.0
            "STANDARD" -> 0.6
            else -> 0.5
        }
    }

    @Action(
        description = "Deliver via drone",
        pre = ["route_calculated"],
        post = ["delivered"],
        costMethod = "droneDeliveryCost",
        valueMethod = "droneDeliveryValue"
    )
    fun deliverByDrone(
        route: Route,
        @Provided blackboard: Blackboard
    ): DeliveryResult {
        return deliveryService.deliverByDrone(route)
    }

    @Cost(name = "droneDeliveryCost")
    fun calculateDroneCost(
        route: Route?,
        @Provided blackboard: Blackboard?
    ): Double {
        val distance = route?.distance ?: return 0.5
        val weather = blackboard?.get("weather_conditions", String::class.java)

        val baseCost = (distance / 100.0).coerceIn(0.1, 0.5)
        val weatherPenalty = if (weather == "STORMY") 0.4 else 0.0

        return (baseCost + weatherPenalty).coerceIn(0.0, 1.0)
    }

    @Cost(name = "droneDeliveryValue")
    fun calculateDroneValue(
        route: Route?,
        @Provided blackboard: Blackboard?
    ): Double {
        val distance = route?.distance ?: return 0.5
        // Drones are valuable for short distances
        return when {
            distance < 10 -> 0.9
            distance < 30 -> 0.7
            distance < 50 -> 0.5
            else -> 0.3
        }
    }

    @Action(
        description = "Deliver via truck",
        pre = ["route_calculated"],
        post = ["delivered"],
        costMethod = "truckDeliveryCost",
        valueMethod = "truckDeliveryValue"
    )
    fun deliverByTruck(
        route: Route,
        @Provided blackboard: Blackboard
    ): DeliveryResult {
        return deliveryService.deliverByTruck(route)
    }

    @Cost(name = "truckDeliveryCost")
    fun calculateTruckCost(
        route: Route?,
        @Provided blackboard: Blackboard?
    ): Double {
        val distance = route?.distance ?: return 0.5
        val traffic = blackboard?.get("current_traffic", String::class.java)

        val baseCost = (distance / 200.0).coerceIn(0.2, 0.7)
        val trafficPenalty = when (traffic) {
            "HEAVY" -> 0.3
            "MODERATE" -> 0.1
            else -> 0.0
        }

        return (baseCost + trafficPenalty).coerceIn(0.0, 1.0)
    }

    @Cost(name = "truckDeliveryValue")
    fun calculateTruckValue(
        route: Route?,
        @Provided blackboard: Blackboard?
    ): Double {
        val distance = route?.distance ?: return 0.5
        val packageWeight = blackboard?.get("package_weight", Double::class.java) ?: 1.0

        // Trucks are valuable for heavy packages and longer distances
        val distanceValue = when {
            distance > 50 -> 0.8
            distance > 30 -> 0.6
            else -> 0.4
        }

        val weightValue = if (packageWeight > 10.0) 0.3 else 0.0

        return (distanceValue + weightValue).coerceIn(0.0, 1.0)
    }

    @Action(
        description = "Send delivery notification",
        pre = ["delivered"],
        cost = 0.1,
        value = 0.8
    )
    fun sendNotification(
        delivery: DeliveryResult,
        @Provided blackboard: Blackboard
    ): NotificationResult {
        val customer = blackboard.get("customer_email", String::class.java)
        return notificationService.send(customer, delivery)
    }
}

Planning Example:

Given this agent and a goal to deliver a package:

  • If the package is light and nearby, the planner prefers drone delivery (high value, low cost)
  • If traffic is heavy and the package is far, the planner might defer the delivery or choose an alternative route
  • If weather is stormy, drone cost increases significantly, making truck delivery more attractive
  • The planner automatically balances these factors to find the optimal plan

Parameter Binding

Control how action parameters are bound to blackboard values and injected services.

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class RequireNameMatch(
    /** Binding name to match (empty string uses parameter name) */
    val value: String = ""
)

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Provided

@RequireNameMatch

The @RequireNameMatch annotation controls parameter binding from the blackboard. By default, the framework attempts to bind parameters by type compatibility. @RequireNameMatch enforces that a parameter must match a specific binding name.

How It Works:

  1. Default Behavior: Without @RequireNameMatch, parameters bind by type from the blackboard
  2. Name Matching: With @RequireNameMatch("name"), parameter only binds to blackboard value with exact name "name"
  3. Parameter Name: With @RequireNameMatch("") or @RequireNameMatch, uses the parameter's own name
  4. Type Safety: Binding still requires type compatibility

Basic Name Matching Example:

@Agent(name = "order-fulfillment", provider = "warehouse", description = "Fulfills orders")
class OrderFulfillmentAgent {

    @Action(
        description = "Pack order items",
        outputBinding = "packed_order"  // Bind output to specific name
    )
    fun packOrder(
        @RequireNameMatch("order") order: Order  // Must match binding name "order"
    ): PackedOrder {
        return warehouse.pack(order)
    }

    @Action(
        description = "Ship packed order",
        pre = ["order_packed"]
    )
    fun shipOrder(
        @RequireNameMatch("packed_order") packedOrder: PackedOrder  // Must match "packed_order"
    ): Shipment {
        return shippingService.ship(packedOrder)
    }
}

Using Parameter Name for Binding:

@Agent(name = "document-processor", provider = "docs", description = "Processes documents")
class DocumentProcessorAgent {

    @Action(
        description = "Extract text from document",
        outputBinding = "extracted_text"
    )
    fun extractText(document: Document): String {
        return textExtractor.extract(document)
    }

    @Action(
        description = "Analyze extracted text",
        pre = ["text_extracted"]
    )
    fun analyzeText(
        @RequireNameMatch extracted_text: String  // Uses parameter name "extracted_text"
    ): Analysis {
        return textAnalyzer.analyze(extracted_text)
    }
}

Avoiding Ambiguity with Multiple Parameters of Same Type:

@Agent(name = "email-service", provider = "communication", description = "Sends emails")
class EmailServiceAgent {

    @Action(description = "Send notification email")
    fun sendEmail(
        @RequireNameMatch("sender") sender: String,      // Binds to "sender" specifically
        @RequireNameMatch("recipient") recipient: String, // Binds to "recipient" specifically
        @RequireNameMatch("subject") subject: String,     // Binds to "subject" specifically
        message: String                                   // Binds by type (any String)
    ): EmailReceipt {
        return emailClient.send(sender, recipient, subject, message)
    }
}

Complex Workflow with Explicit Bindings:

@Agent(name = "data-pipeline", provider = "etl", description = "ETL data pipeline")
class DataPipelineAgent {

    @Action(
        description = "Extract data from source",
        outputBinding = "raw_data"
    )
    fun extract(sourceUrl: String): RawData {
        return extractor.extract(sourceUrl)
    }

    @Action(
        description = "Transform data",
        pre = ["data_extracted"],
        outputBinding = "transformed_data"
    )
    fun transform(
        @RequireNameMatch("raw_data") data: RawData  // Requires "raw_data" from extract
    ): TransformedData {
        return transformer.transform(data)
    }

    @Action(
        description = "Load data to destination",
        pre = ["data_transformed"]
    )
    fun load(
        @RequireNameMatch("transformed_data") data: TransformedData  // Requires "transformed_data"
    ): LoadResult {
        return loader.load(data)
    }
}

@Provided

The @Provided annotation marks parameters that should be injected by the framework rather than bound from the blackboard. This is used for context services, platform objects, and dependencies.

How It Differs from Blackboard Binding:

  • @Provided: Injects framework services (ActionContext, Blackboard, PromptRunner, etc.)
  • Blackboard Binding: Retrieves data values stored on the blackboard from previous actions
  • @RequireNameMatch: Controls blackboard binding behavior (not used with @Provided)

Common Provided Services:

  • ActionContext: Provides access to LLM, progress updates, and execution context
  • Blackboard: Provides direct access to blackboard for reading/writing state
  • PromptRunner: For making LLM calls
  • Warehouse: Access to warehouse services (if registered)
  • Custom registered services

Injecting ActionContext:

@Agent(name = "content-generator", provider = "ai", description = "Generates content with AI")
class ContentGeneratorAgent {

    @Action(description = "Generate blog post with LLM")
    fun generateBlogPost(
        topic: String,
        @Provided context: ActionContext  // Injected by framework
    ): BlogPost {
        // Access LLM through context
        val content = context.promptRunner()
            .creating(String::class.java)
            .fromPrompt("Write a blog post about: $topic")

        // Update progress
        context.updateProgress("Generated ${content.length} characters")

        return BlogPost(topic, content)
    }
}

Injecting Blackboard for State Management:

@Agent(name = "session-manager", provider = "state", description = "Manages session state")
class SessionManagerAgent {

    @Action(description = "Initialize session")
    fun initializeSession(
        userId: String,
        @Provided blackboard: Blackboard  // Direct blackboard access
    ): Session {
        val session = Session(userId, System.currentTimeMillis())

        // Store session data on blackboard
        blackboard.put("session_id", session.id)
        blackboard.put("session_start_time", session.startTime)
        blackboard.put("user_id", userId)

        return session
    }

    @Action(description = "Update session activity")
    fun updateActivity(
        activity: String,
        @Provided blackboard: Blackboard
    ): ActivityRecord {
        val sessionId = blackboard.get("session_id", String::class.java)
        val record = ActivityRecord(sessionId, activity, System.currentTimeMillis())

        // Update blackboard state
        blackboard.put("last_activity", activity)
        blackboard.put("last_activity_time", record.timestamp)

        return record
    }
}

Injecting Multiple Services:

@Agent(name = "analytics-processor", provider = "analytics", description = "Processes analytics")
class AnalyticsProcessorAgent(
    private val analyticsService: AnalyticsService
) {

    @Action(description = "Process user events with context")
    fun processEvents(
        events: List<Event>,
        @Provided context: ActionContext,
        @Provided blackboard: Blackboard
    ): AnalyticsReport {
        // Access blackboard state
        val userId = blackboard.get("user_id", String::class.java)
        val sessionId = blackboard.get("session_id", String::class.java)

        // Update progress through context
        context.updateProgress("Processing ${events.size} events")

        // Use LLM for insight generation
        val insights = context.promptRunner()
            .creating(Insights::class.java)
            .fromPrompt("""
                Analyze these user events and extract insights:
                ${events.joinToString("\n") { it.description }}
            """)

        // Store results on blackboard
        blackboard.put("analytics_insights", insights)

        return AnalyticsReport(userId, sessionId, events, insights)
    }
}

Combining @Provided with @RequireNameMatch:

@Agent(name = "order-processor", provider = "commerce", description = "Processes orders")
class OrderProcessorAgent {

    @Action(
        description = "Process order with payment",
        outputBinding = "processed_order"
    )
    fun processOrder(
        @RequireNameMatch("customer_order") order: Order,  // From blackboard
        @RequireNameMatch("payment_method") payment: PaymentMethod,  // From blackboard
        @Provided context: ActionContext,  // Injected service
        @Provided blackboard: Blackboard   // Injected service
    ): ProcessedOrder {
        context.updateProgress("Processing order ${order.id}")

        // Process payment
        val paymentResult = paymentService.charge(payment, order.total)
        blackboard.put("payment_result", paymentResult)

        // Process order
        val processed = orderService.process(order)
        context.updateProgress("Order ${order.id} processed successfully")

        return processed
    }
}

Using @Provided in Cost Methods:

@Agent(name = "resource-optimizer", provider = "system", description = "Optimizes resources")
class ResourceOptimizerAgent {

    @Action(
        description = "Allocate computing resources",
        costMethod = "allocationCost"
    )
    fun allocateResources(
        resourceRequest: ResourceRequest,
        @Provided context: ActionContext
    ): AllocationResult {
        return resourceAllocator.allocate(resourceRequest)
    }

    @Cost(name = "allocationCost")
    fun calculateCost(
        resourceRequest: ResourceRequest?,
        @Provided blackboard: Blackboard?  // Inject blackboard in cost method
    ): Double {
        // Access current resource utilization
        val currentUtilization = blackboard?.get("cpu_utilization", Double::class.java) ?: 0.5
        val requestedCpu = resourceRequest?.cpuCores ?: 1

        // Cost increases with system load and request size
        val baseCost = requestedCpu / 100.0
        val loadMultiplier = 1.0 + currentUtilization

        return (baseCost * loadMultiplier).coerceIn(0.0, 1.0)
    }
}

Complete Example with All Parameter Binding Types:

@Agent(
    name = "intelligent-workflow",
    provider = "automation",
    description = "Intelligent workflow automation"
)
class IntelligentWorkflowAgent(
    private val workflowEngine: WorkflowEngine
) {

    @Action(
        description = "Analyze workflow requirements",
        outputBinding = "workflow_analysis"
    )
    fun analyzeWorkflow(
        @RequireNameMatch("workflow_definition") definition: WorkflowDefinition,
        @Provided context: ActionContext,
        @Provided blackboard: Blackboard
    ): WorkflowAnalysis {
        context.updateProgress("Analyzing workflow: ${definition.name}")

        // Use LLM for analysis
        val analysis = context.promptRunner()
            .creating(WorkflowAnalysis::class.java)
            .fromPrompt("""
                Analyze this workflow and identify optimization opportunities:
                ${definition.toDescription()}
            """)

        // Store on blackboard
        blackboard.put("analysis_timestamp", System.currentTimeMillis())

        return analysis
    }

    @Action(
        description = "Optimize workflow based on analysis",
        pre = ["workflow_analyzed"],
        outputBinding = "optimized_workflow"
    )
    fun optimizeWorkflow(
        @RequireNameMatch("workflow_analysis") analysis: WorkflowAnalysis,
        @RequireNameMatch("workflow_definition") definition: WorkflowDefinition,
        @Provided blackboard: Blackboard
    ): OptimizedWorkflow {
        val timestamp = blackboard.get("analysis_timestamp", Long::class.java)

        val optimized = workflowEngine.optimize(definition, analysis)

        blackboard.put("optimization_timestamp", System.currentTimeMillis())
        blackboard.put("optimization_duration", System.currentTimeMillis() - timestamp)

        return optimized
    }

    @Action(
        description = "Execute optimized workflow",
        pre = ["workflow_optimized"],
        costMethod = "executionCost",
        valueMethod = "executionValue"
    )
    fun executeWorkflow(
        @RequireNameMatch("optimized_workflow") workflow: OptimizedWorkflow,
        @Provided context: ActionContext,
        @Provided blackboard: Blackboard
    ): ExecutionResult {
        context.updateProgress("Executing workflow: ${workflow.name}")

        val result = workflowEngine.execute(workflow)

        blackboard.put("execution_result", result)
        context.updateProgress("Workflow completed: ${result.status}")

        return result
    }

    @Cost(name = "executionCost")
    fun calculateExecutionCost(
        @RequireNameMatch("optimized_workflow") workflow: OptimizedWorkflow?,
        @Provided blackboard: Blackboard?
    ): Double {
        val complexity = workflow?.complexity ?: return 0.5
        val systemLoad = blackboard?.get("system_load", Double::class.java) ?: 0.5

        return ((complexity / 100.0) * (1.0 + systemLoad)).coerceIn(0.0, 1.0)
    }

    @Cost(name = "executionValue")
    fun calculateExecutionValue(
        @RequireNameMatch("optimized_workflow") workflow: OptimizedWorkflow?,
        @Provided blackboard: Blackboard?
    ): Double {
        val priority = workflow?.priority ?: return 0.5
        val optimizationGain = blackboard?.get("optimization_duration", Long::class.java)?.let {
            (it / 1000.0).coerceIn(0.0, 1.0)
        } ?: 0.5

        return ((priority / 10.0) + optimizationGain) / 2.0
    }
}

Tool Group Requirements

Specify tool groups required by actions.

annotation class ToolGroupRequirement(
    val group: String,
    val required: Boolean = true
)

Tool Group Example:

@Agent(name = "file-processor", provider = "storage", description = "Processes files")
class FileProcessorAgent {

    @Action(
        description = "Read and process file",
        toolGroups = ["file-operations", "text-processing"]
    )
    fun processFile(path: String, context: ActionContext): ProcessedFile {
        val content = context.withToolGroup("file-operations") {
            fileTools.read(path)
        }
        return processor.process(content)
    }
}

Action Retry Policies

Configure retry behavior for failed actions.

enum class ActionRetryPolicy {
    /** Fire only once (maxAttempts = 1) */
    FIRE_ONCE,
    /** Default retry policy using ActionQos defaults */
    DEFAULT
}

Retry Example:

@Agent(name = "api-client", provider = "integration", description = "Calls external APIs")
class ApiClientAgent {

    @Action(
        description = "Call external API with retry",
        actionRetryPolicy = ActionRetryPolicy.DEFAULT
    )
    fun callApi(endpoint: String): ApiResponse {
        return externalApi.call(endpoint)
    }

    @Action(
        description = "Call critical API with custom retry",
        actionRetryPolicyExpression = "#{retryCount < 5 && error.isTransient()}"
    )
    fun callCriticalApi(endpoint: String): ApiResponse {
        return criticalApi.call(endpoint)
    }
}

Types

/** Type alias for values constrained to 0.0-1.0 range */
typealias ZeroToOne = Double

interface Action {
    val name: String
    val description: String
    val preconditions: List<String>
    val postconditions: List<String>
    val cost: Double
    val value: Double
    val canRerun: Boolean
}

interface Goal {
    val name: String
    val description: String
    val value: Double
    val tags: List<String>
}

enum class ActionRetryPolicy {
    /** Fire only once (maxAttempts = 1) */
    FIRE_ONCE,
    /** Default retry policy using ActionQos defaults */
    DEFAULT
}

interface ToolGroupRequirement {
    val group: String
    val required: Boolean
}

Install with Tessl CLI

npx tessl i tessl/maven-com-embabel-agent--embabel-agent-api

docs

actions-goals.md

agent-definition.md

builtin-tools.md

chat.md

conditions.md

events.md

human-in-the-loop.md

index.md

invocation.md

io-binding.md

llm-interaction.md

models.md

planning-workflows.md

platform-management.md

runtime-context.md

state-management.md

streaming.md

subagents.md

tools.md

type-system.md

typed-operations.md

validation.md

tile.json