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
The platform and process management APIs provide the core infrastructure for running and managing agent processes. The AgentPlatform acts as the execution environment for agents, while AgentProcess represents individual running instances with their own lifecycle, state, and control mechanisms.
The AgentPlatform is the central execution environment for agents. It manages agent deployment, process creation, and lifecycle operations.
import com.embabel.agent.core.AgentPlatform
import com.embabel.agent.core.Agent
import com.embabel.agent.core.AgentProcess
import com.embabel.agent.core.ProcessOptions
import com.embabel.agent.api.common.PlatformServices
import com.embabel.agent.spi.ToolGroupResolver
import java.util.concurrent.CompletableFuture
interface AgentPlatform : AgentScope {
/** Core services provided by the platform */
val platformServices: PlatformServices
/** Resolver for tool groups */
val toolGroupResolver: ToolGroupResolver
/** Find an agent process by ID (running processes only) */
fun getAgentProcess(id: String): AgentProcess?
/** Kill an agent process by ID, returns the killed process or null */
fun killAgentProcess(id: String): AgentProcess?
/** Get all deployed agents */
fun agents(): List<Agent>
/** Deploy an agent to the platform */
fun deploy(agent: Agent): AgentPlatform
/** Deploy an agent scope to the platform */
fun deploy(agentScope: AgentScope): AgentPlatform
/** Run an agent with the given options and bindings */
fun runAgentFrom(
agent: Agent,
processOptions: ProcessOptions = ProcessOptions(),
bindings: Map<String, Any>
): AgentProcess
/** Create an agent process without starting it (status: NOT_STARTED) */
fun createAgentProcess(
agent: Agent,
processOptions: ProcessOptions,
bindings: Map<String, Any>
): AgentProcess
/** Create an agent process and bind objects to the blackboard */
fun createAgentProcessFrom(
agent: Agent,
processOptions: ProcessOptions,
vararg objectsToAdd: Any
): AgentProcess
/** Run an agent process in the background asynchronously */
fun start(agentProcess: AgentProcess): CompletableFuture<AgentProcess>
/** Create a child process from a parent agent process */
fun createChildProcess(
agent: Agent,
parentAgentProcess: AgentProcess
): AgentProcess
/** All domain types from deployed agents */
override val domainTypes: Collection<DomainType>
/** All actions from deployed agents */
override val actions: List<Action>
/** All goals from deployed agents */
override val goals: Set<Goal>
/** All conditions from deployed agents */
override val conditions: Set<Condition>
}Basic Usage:
@Action(description = "Run data processing agent")
fun processData(
dataSource: DataSource,
platform: AgentPlatform,
context: ActionContext
): ProcessingResult {
// Create process with custom options
val process = platform.createAgentProcessFrom(
agent = dataProcessingAgent,
processOptions = ProcessOptions()
.withBudget(Budget(cost = 1.0, actions = 30))
.withVerbosity(Verbosity().showPlanning()),
dataSource,
context.blackboard()
)
// Run the process
val completedProcess = process.run()
// Extract result
return completedProcess.resultOfType<ProcessingResult>()
}Asynchronous Execution:
@Action(description = "Start analysis in background")
fun startBackgroundAnalysis(
data: Dataset,
platform: AgentPlatform
): String {
val process = platform.createAgentProcess(
agent = analysisAgent,
processOptions = ProcessOptions(),
bindings = mapOf("dataset" to data)
)
// Start async and return immediately
val future = platform.start(process)
// Can monitor later
future.thenAccept { completed ->
println("Analysis completed: ${completed.id}")
}
return "Started analysis process: ${process.id}"
}Represents a running instance of an agent with its own state, lifecycle, and execution control.
import com.embabel.agent.core.AgentProcess
import com.embabel.agent.core.AgentProcessStatusCode
import com.embabel.agent.core.Agent
import com.embabel.agent.core.Blackboard
import com.embabel.agent.core.ProcessOptions
import com.embabel.agent.core.ProcessContext
import com.embabel.agent.core.LlmInvocation
import com.embabel.agent.api.event.ProcessKilledEvent
import com.embabel.plan.Planner
import com.embabel.plan.WorldState
import java.time.Duration
import java.time.Instant
interface AgentProcess : Blackboard, Timestamped, Timed,
OperationStatus<AgentProcessStatusCode>, LlmInvocationHistory {
/** Unique identifier for this process */
val id: String
/** The agent this process is running */
val agent: Agent
/** Current status of the process */
override val status: AgentProcessStatusCode
/** The blackboard for this process */
val blackboard: Blackboard
/** Options the process was started with */
val processOptions: ProcessOptions
/** The planner used by this process */
val planner: Planner<*, *, *>
/** History of actions taken by this process */
val history: List<ActionInvocation>
/** Goal of this process (may be null for utility processes) */
val goal: com.embabel.plan.Goal?
/** ID of the parent AgentProcess, if any */
val parentId: String?
/** Last world state used to plan the next action */
val lastWorldState: WorldState?
/** When the process started */
override val timestamp: Instant
/** How long the process has been running */
override val runningTime: Duration
/** Is the process finished (completed, failed, killed, or terminated)? */
val finished: Boolean
/** Failure information if the process failed */
val failureInfo: Any?
/** Process context */
val processContext: ProcessContext
/** Run the process as far as possible */
fun run(): AgentProcess
/** Perform the next step only (single action) */
fun tick(): AgentProcess
/** Kill this process and return kill event if successful */
fun kill(): ProcessKilledEvent?
/** Pause the process (implementation-specific) */
fun pause()
/** Resume a paused process (implementation-specific) */
fun resume()
/** Return a serializable status report */
fun statusReport(): AgentProcessStatusReport
/** Get the result of a specific type (process must be completed) */
fun <O> resultOfType(outputClass: Class<O>): O
/** Get a variable value from the blackboard */
fun getValue(variable: String, type: String): Any?
/** Record an LLM invocation for tracking */
fun recordLlmInvocation(llmInvocation: LlmInvocation)
companion object {
/** Get the current agent process for this thread (only during tool calls) */
@JvmStatic
fun get(): AgentProcess?
}
}
/** Convenience extension function for reified type result extraction */
inline fun <reified O> AgentProcess.resultOfType(): OBasic Process Execution:
@Action(description = "Execute validation process")
fun validateDocument(
document: Document,
platform: AgentPlatform
): ValidationReport {
// Create process
val process = platform.createAgentProcess(
agent = validationAgent,
processOptions = ProcessOptions(),
bindings = mapOf("document" to document)
)
// Run synchronously
val completed = process.run()
// Check status and extract result
return when (completed.status) {
AgentProcessStatusCode.COMPLETED ->
completed.resultOfType<ValidationReport>()
AgentProcessStatusCode.FAILED ->
throw ProcessException("Validation failed: ${completed.failureInfo}")
else ->
throw ProcessException("Unexpected status: ${completed.status}")
}
}Step-by-Step Execution:
@Action(description = "Monitor process execution step-by-step")
fun monitoredExecution(
task: Task,
platform: AgentPlatform
): ExecutionTrace {
val process = platform.createAgentProcess(
agent = taskAgent,
processOptions = ProcessOptions().withVerbosity(Verbosity().debug()),
bindings = mapOf("task" to task)
)
val trace = mutableListOf<String>()
// Execute step by step
while (!process.finished) {
process.tick()
// Log each action
if (process.history.isNotEmpty()) {
val lastAction = process.history.last()
trace.add("Action: ${lastAction.actionName} (${lastAction.runningTime.toMillis()}ms)")
}
// Check status
trace.add("Status: ${process.status}")
}
return ExecutionTrace(
processId = process.id,
steps = trace,
finalStatus = process.status
)
}Thread-Local Access:
@Tool(description = "Get current process information")
fun getCurrentProcessInfo(): ProcessInfo {
val process = AgentProcess.get()
?: throw IllegalStateException("No agent process in current thread")
return ProcessInfo(
id = process.id,
agentName = process.agent.name,
status = process.status.name,
actionsExecuted = process.history.size,
runningTime = process.runningTime
)
}Status codes for agent process lifecycle.
enum class AgentProcessStatusCode {
/** The process has not started yet */
NOT_STARTED,
/** The process is running without any known problems */
RUNNING,
/** The process has completed successfully */
COMPLETED,
/** Game over. The process has failed */
FAILED,
/** The process has been killed by an early termination policy */
TERMINATED,
/** The process has been killed by the user or platform, from outside */
KILLED,
/** The process cannot formulate a plan to progress (may be temporary) */
STUCK,
/** The process is waiting for user input or another external event */
WAITING,
/** The process has paused due to scheduling policy */
PAUSED
}Status Checking:
@Action(description = "Check process health")
fun checkProcessHealth(processId: String, platform: AgentPlatform): HealthStatus {
val process = platform.getAgentProcess(processId)
?: return HealthStatus.NOT_FOUND
return when (process.status) {
AgentProcessStatusCode.RUNNING -> HealthStatus.HEALTHY
AgentProcessStatusCode.WAITING, AgentProcessStatusCode.PAUSED -> HealthStatus.IDLE
AgentProcessStatusCode.STUCK -> HealthStatus.DEGRADED
AgentProcessStatusCode.COMPLETED -> HealthStatus.COMPLETED
else -> HealthStatus.FAILED
}
}Configuration for agent process execution.
import com.embabel.agent.core.ProcessOptions
import com.embabel.agent.core.Verbosity
import com.embabel.agent.core.Budget
import com.embabel.agent.core.ProcessControl
import com.embabel.agent.core.Identities
import com.embabel.agent.core.ContextId
import com.embabel.agent.core.Blackboard
import com.embabel.agent.api.channel.OutputChannel
import com.embabel.agent.api.event.AgenticEventListener
import com.embabel.agent.api.common.PlannerType
data class ProcessOptions(
/** Context ID for connecting to external resources and persistence */
val contextId: ContextId? = null,
/** User identities associated with this process */
val identities: Identities = Identities(),
/** Existing blackboard to use (will be modified during execution) */
val blackboard: Blackboard? = null,
/** Verbosity settings for logging */
val verbosity: Verbosity = Verbosity(),
/** Budget constraints for the process */
val budget: Budget = Budget(),
/** Process control settings (defaults based on budget) */
val processControl: ProcessControl = ProcessControl(
toolDelay = Delay.NONE,
operationDelay = Delay.NONE,
earlyTerminationPolicy = budget.earlyTerminationPolicy()
),
/** Whether to prune the agent to only relevant actions */
val prune: Boolean = false,
/** Additional event listeners for this process */
val listeners: List<AgenticEventListener> = emptyList(),
/** Custom output channel for process output */
val outputChannel: OutputChannel = DevNullOutputChannel,
/** Type of planner to use (default: GOAP) */
val plannerType: PlannerType = PlannerType.GOAP
) {
/** Get context ID as string (Java interop) */
fun getContextIdString(): String?
/** Wither methods for fluent configuration */
fun withContextId(contextId: ContextId?): ProcessOptions
fun withContextId(contextId: String?): ProcessOptions
fun withIdentities(identities: Identities): ProcessOptions
fun withBlackboard(blackboard: Blackboard?): ProcessOptions
fun withVerbosity(verbosity: Verbosity): ProcessOptions
fun withBudget(budget: Budget): ProcessOptions
fun withProcessControl(processControl: ProcessControl): ProcessOptions
fun withAdditionalEarlyTerminationPolicy(policy: EarlyTerminationPolicy): ProcessOptions
fun withPrune(prune: Boolean): ProcessOptions
fun withListeners(listeners: List<AgenticEventListener>): ProcessOptions
fun withListener(listener: AgenticEventListener): ProcessOptions
fun withOutputChannel(outputChannel: OutputChannel): ProcessOptions
fun withPlannerType(plannerType: PlannerType): ProcessOptions
companion object {
@JvmField
val DEFAULT = ProcessOptions()
}
}Configuration Examples:
// Minimal configuration
val basicOptions = ProcessOptions()
// Custom verbosity
val verboseOptions = ProcessOptions()
.withVerbosity(
Verbosity()
.showPrompts()
.showLlmResponses()
.showPlanning()
)
// Budget constraints
val budgetedOptions = ProcessOptions()
.withBudget(
Budget()
.withCost(0.5)
.withActions(20)
.withTokens(50000)
)
// Custom termination policy
val customOptions = ProcessOptions()
.withAdditionalEarlyTerminationPolicy(
EarlyTerminationPolicy.ON_STUCK
)
// With event listener
val monitoredOptions = ProcessOptions()
.withListener(object : AgenticEventListener {
override fun onEvent(event: AgenticEvent) {
println("Event: ${event.javaClass.simpleName}")
}
})
// Complex configuration
val fullOptions = ProcessOptions()
.withContextId("session-123")
.withIdentities(Identities(forUser = currentUser))
.withBudget(Budget(cost = 1.0, actions = 50))
.withVerbosity(Verbosity().showPlanning())
.withPlannerType(PlannerType.GOAP)
.withPrune(true)Controls logging and debugging output detail.
data class Verbosity(
val showPrompts: Boolean = false,
val showLlmResponses: Boolean = false,
val debug: Boolean = false,
val showPlanning: Boolean = false
) : LlmVerbosity {
/** Derived property: show long plans if any verbosity is enabled */
val showLongPlans: Boolean
/** Wither methods for fluent configuration */
fun withShowPrompts(showPrompts: Boolean): Verbosity
fun showPrompts(): Verbosity
fun withShowLlmResponses(showLlmResponses: Boolean): Verbosity
fun showLlmResponses(): Verbosity
fun withDebug(debug: Boolean): Verbosity
fun debug(): Verbosity
fun withShowPlanning(showPlanning: Boolean): Verbosity
fun showPlanning(): Verbosity
companion object {
@JvmField
val DEFAULT = Verbosity()
}
}Verbosity Examples:
// Show all details
val maxVerbosity = Verbosity()
.showPrompts()
.showLlmResponses()
.showPlanning()
.debug()
// Just planning
val planningOnly = Verbosity().showPlanning()
// Debug mode
val debugMode = Verbosity().debug()
// Production mode (default)
val production = Verbosity.DEFAULTDefine cost and resource limits for agent processes.
data class Budget(
/** Maximum cost in USD */
val cost: Double = DEFAULT_COST_LIMIT,
/** Maximum number of actions before termination */
val actions: Int = DEFAULT_ACTION_LIMIT,
/** Maximum number of tokens before termination */
val tokens: Int = DEFAULT_TOKEN_LIMIT
) {
/** Convert budget to early termination policy */
fun earlyTerminationPolicy(): EarlyTerminationPolicy
/** Wither methods for fluent configuration */
fun withCost(cost: Double): Budget
fun withActions(actions: Int): Budget
fun withTokens(tokens: Int): Budget
companion object {
const val DEFAULT_COST_LIMIT = 2.0
const val DEFAULT_ACTION_LIMIT = 50
const val DEFAULT_TOKEN_LIMIT = 1000000
@JvmField
val DEFAULT = Budget()
}
}Budget Examples:
// Conservative budget
val conservative = Budget()
.withCost(0.25)
.withActions(10)
// Standard budget
val standard = Budget.DEFAULT
// Generous budget
val generous = Budget()
.withCost(5.0)
.withActions(100)
.withTokens(2000000)
// Cost-focused
val costFocused = Budget()
.withCost(1.0)
.withActions(Int.MAX_VALUE)
// Action-limited
val actionLimited = Budget()
.withCost(Double.MAX_VALUE)
.withActions(25)User identity information for process execution.
data class Identities(
/** User for whom the process is running */
val forUser: User? = null,
/** User under which the process is running */
val runAs: User? = null
) {
/** Wither methods for fluent configuration */
fun withForUser(forUser: User?): Identities
fun withRunAs(runAs: User?): Identities
companion object {
@JvmField
val DEFAULT = Identities()
}
}Identity Examples:
// Run on behalf of user
val userIdentities = Identities()
.withForUser(currentUser)
// Run as service account
val serviceIdentities = Identities()
.withRunAs(serviceAccount)
// Both identities
val fullIdentities = Identities(
forUser = requestingUser,
runAs = executionUser
)Control timing between operations.
enum class Delay {
/** No delay */
NONE,
/** Medium delay between operations */
MEDIUM,
/** Long delay between operations */
LONG
}Fine-grained control over process execution behavior.
data class ProcessControl(
/** Delay between tool invocations */
val toolDelay: Delay = Delay.NONE,
/** Delay between operations */
val operationDelay: Delay = Delay.NONE,
/** Policy for early termination */
val earlyTerminationPolicy: EarlyTerminationPolicy =
EarlyTerminationPolicy.maxActions(100)
) {
/** Wither methods for fluent configuration */
fun withToolDelay(toolDelay: Delay): ProcessControl
fun withOperationDelay(operationDelay: Delay): ProcessControl
fun withEarlyTerminationPolicy(policy: EarlyTerminationPolicy): ProcessControl
fun withAdditionalEarlyTerminationPolicy(policy: EarlyTerminationPolicy): ProcessControl
}ProcessControl Examples:
// Rate-limited execution
val rateLimited = ProcessControl()
.withToolDelay(Delay.MEDIUM)
.withOperationDelay(Delay.MEDIUM)
// Custom termination
val customTermination = ProcessControl()
.withEarlyTerminationPolicy(
EarlyTerminationPolicy.firstOf(
EarlyTerminationPolicy.maxActions(30),
EarlyTerminationPolicy.hardBudgetLimit(1.0),
EarlyTerminationPolicy.ON_STUCK
)
)
// Add termination policy
val enhanced = ProcessControl()
.withAdditionalEarlyTerminationPolicy(
EarlyTerminationPolicy.maxTokens(100000)
)Define custom termination conditions for agent processes.
import com.embabel.agent.core.EarlyTerminationPolicy
import com.embabel.agent.core.EarlyTermination
interface EarlyTerminationPolicy {
/** Name of this policy (defaults to class name) */
val name: String
/** Check if the process should be terminated early */
fun shouldTerminate(agentProcess: AgentProcess): EarlyTermination?
companion object {
/** Terminate when process becomes stuck */
@JvmStatic
val ON_STUCK: EarlyTerminationPolicy
/** Terminate after maximum number of actions */
@JvmStatic
fun maxActions(maxActions: Int): EarlyTerminationPolicy
/** Terminate after maximum tokens consumed */
@JvmStatic
fun maxTokens(maxTokens: Int): EarlyTerminationPolicy
/** Combine multiple policies (first match wins) */
@JvmStatic
fun firstOf(vararg policies: EarlyTerminationPolicy): EarlyTerminationPolicy
/** Hard budget limit (last resort to prevent runaway costs) */
@JvmStatic
fun hardBudgetLimit(budget: Double): EarlyTerminationPolicy
}
}
data class EarlyTermination(
val agentProcess: AgentProcess,
val error: Boolean,
val reason: String,
val policy: EarlyTerminationPolicy
)Custom Termination Policy:
class TimeBasedTerminationPolicy(
private val maxDuration: Duration
) : EarlyTerminationPolicy {
override val name = "TimeLimit"
override fun shouldTerminate(agentProcess: AgentProcess): EarlyTermination? {
return if (agentProcess.runningTime > maxDuration) {
EarlyTermination(
agentProcess = agentProcess,
error = true,
reason = "Process exceeded time limit of ${maxDuration.toMinutes()} minutes",
policy = this
)
} else null
}
}
// Use custom policy
val options = ProcessOptions()
.withProcessControl(
ProcessControl()
.withEarlyTerminationPolicy(
TimeBasedTerminationPolicy(Duration.ofMinutes(5))
)
)/** History element for action invocations */
data class ActionInvocation(
val actionName: String,
val timestamp: Instant,
val runningTime: Duration
)
/** Serializable status report for agent processes */
data class AgentProcessStatusReport(
val id: String,
val status: AgentProcessStatusCode,
val timestamp: Instant,
val runningTime: Duration
)
/** Context ID for process persistence and resource connection */
@JvmInline
value class ContextId(val value: String)
interface Timestamped {
val timestamp: Instant
}
interface Timed {
val runningTime: Duration
}
interface OperationStatus<S> : Timed where S : Enum<S> {
val status: S
}import com.embabel.agent.core.*
import com.embabel.agent.api.annotation.Action
import java.time.Duration
@Action(description = "Execute complex data pipeline with monitoring")
fun executeDataPipeline(
pipeline: DataPipeline,
platform: AgentPlatform,
context: ActionContext
): PipelineResult {
// Configure process with comprehensive options
val options = ProcessOptions()
.withContextId("pipeline-${pipeline.id}")
.withIdentities(Identities(forUser = context.user()))
.withBudget(
Budget()
.withCost(2.0)
.withActions(75)
.withTokens(500000)
)
.withVerbosity(
Verbosity()
.showPlanning()
.withDebug(pipeline.debugMode)
)
.withProcessControl(
ProcessControl()
.withToolDelay(Delay.NONE)
.withAdditionalEarlyTerminationPolicy(
EarlyTerminationPolicy.ON_STUCK
)
)
.withListener(object : AgenticEventListener {
override fun onEvent(event: AgenticEvent) {
when (event) {
is ActionCompletedEvent ->
context.log("Action completed: ${event.actionName}")
is EarlyTermination ->
context.log("Early termination: ${event.reason}")
}
}
})
// Create and run process
val process = platform.createAgentProcess(
agent = pipelineAgent,
processOptions = options,
bindings = mapOf(
"pipeline" to pipeline,
"config" to pipeline.config
)
)
// Execute with monitoring
val result = when {
pipeline.async -> {
// Run asynchronously
val future = platform.start(process)
PipelineResult.Async(processId = process.id, future = future)
}
pipeline.stepByStep -> {
// Execute step-by-step with monitoring
val steps = mutableListOf<StepResult>()
while (!process.finished) {
process.tick()
if (process.history.isNotEmpty()) {
steps.add(StepResult(
action = process.history.last().actionName,
status = process.status
))
}
}
PipelineResult.Stepped(steps = steps, process = process)
}
else -> {
// Synchronous execution
val completed = process.run()
PipelineResult.Completed(process = completed)
}
}
// Handle completion
return when (result) {
is PipelineResult.Completed -> {
when (result.process.status) {
AgentProcessStatusCode.COMPLETED ->
result.process.resultOfType<PipelineResult>()
AgentProcessStatusCode.FAILED ->
throw PipelineException("Pipeline failed", result.process.failureInfo)
AgentProcessStatusCode.TERMINATED ->
throw BudgetExceededException("Pipeline terminated: budget exceeded")
else ->
throw PipelineException("Unexpected status: ${result.process.status}")
}
}
else -> result
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-embabel-agent--embabel-agent-api@0.3.0docs