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
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.
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)
}
}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)
}
}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:
Double@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:
value - cost to find optimal plansComplete 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:
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 ProvidedThe @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:
@RequireNameMatch, parameters bind by type from the blackboard@RequireNameMatch("name"), parameter only binds to blackboard value with exact name "name"@RequireNameMatch("") or @RequireNameMatch, uses the parameter's own nameBasic 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)
}
}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:
Common Provided Services:
ActionContext: Provides access to LLM, progress updates, and execution contextBlackboard: Provides direct access to blackboard for reading/writing statePromptRunner: For making LLM callsWarehouse: Access to warehouse services (if registered)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
}
}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)
}
}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)
}
}/** 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@0.3.0docs