Base starter module for the Embabel Agent Framework providing core dependencies for building agentic flows on the JVM with Spring Boot integration and GOAP-based intelligent path finding.
Step-by-step guide for implementing human-in-the-loop patterns with WaitFor utility class.
Request structured form input from users.
import com.embabel.agent.api.annotation.WaitFor;
import com.embabel.agent.api.hitl.FormBindingRequest;
import com.embabel.agent.api.annotation.Action;
// Define data class for form
public class UserDetails {
private String name;
private String email;
private int age;
// Getters and setters
}
@Agent(description = "User registration agent")
public class RegistrationAgent {
@Action(description = "Gather user details")
public Result gatherUserDetails() {
// Request form from user
FormBindingRequest<UserDetails> request =
WaitFor.formSubmission("Enter Your Details", UserDetails.class);
// Wait for user to submit the form
UserDetails details = request.await();
// Process the submitted details
return processDetails(details);
}
}import com.embabel.agent.api.annotation.WaitFor
import com.embabel.agent.api.annotation.Action
// Data class for form
data class UserDetails(
val name: String,
val email: String,
val age: Int
)
@Agent(description = "Registration agent")
class RegistrationAgent {
@Action(description = "Gather user details")
fun gatherUserDetails(): Result {
val request = WaitFor.formSubmission(
"Enter Your Details",
UserDetails::class.java
)
val details = request.await()
return processDetails(details)
}
}Request yes/no confirmation from users.
import com.embabel.agent.api.annotation.WaitFor;
import com.embabel.agent.api.hitl.ConfirmationRequest;
@Agent(description = "Resource manager")
public class ResourceAgent {
@Action(description = "Delete resource")
public Result deleteResource(Resource resource) {
// Request confirmation
ConfirmationRequest<Resource> request =
WaitFor.confirmation(resource, "Delete this resource permanently?");
// Wait for user confirmation
boolean confirmed = request.await();
if (confirmed) {
// Proceed with deletion
return performDeletion(resource);
} else {
// User cancelled
return Result.cancelled("Deletion cancelled by user");
}
}
}import com.embabel.agent.api.annotation.WaitFor
@Agent(description = "Resource manager")
class ResourceAgent {
@Action(description = "Delete resource")
fun deleteResource(resource: Resource): Result {
val request = WaitFor.confirmation(
resource,
"Delete this resource permanently?"
)
val confirmed = request.await()
return if (confirmed) {
performDeletion(resource)
} else {
Result.cancelled("Deletion cancelled by user")
}
}
}Use validation annotations for form data classes.
import javax.validation.constraints.*;
public class RegistrationForm {
@NotBlank
@Size(min = 3, max = 50)
private String username;
@Email
@NotBlank
private String email;
@Min(18)
@Max(120)
private int age;
@Pattern(regexp = "^\\+?[0-9]{10,15}$")
private String phone;
// Getters and setters
}
@Agent(description = "User registration")
public class UserRegistrationAgent {
@Action(description = "Register new user")
public User registerUser() {
FormBindingRequest<RegistrationForm> request =
WaitFor.formSubmission("User Registration", RegistrationForm.class);
RegistrationForm form = request.await();
// Validation happens automatically
// If validation fails, user is prompted to correct errors
return createUser(form);
}
}import javax.validation.constraints.*
data class RegistrationForm(
@field:NotBlank
@field:Size(min = 3, max = 50)
val username: String,
@field:Email
@field:NotBlank
val email: String,
@field:Min(18)
@field:Max(120)
val age: Int,
@field:Pattern(regexp = "^\\+?[0-9]{10,15}$")
val phone: String
)
@Agent(description = "User registration")
class UserRegistrationAgent {
@Action(description = "Register new user")
fun registerUser(): User {
val request = WaitFor.formSubmission(
"User Registration",
RegistrationForm::class.java
)
val form = request.await()
return createUser(form)
}
}Combine multiple HITL interactions in a workflow.
import com.embabel.agent.api.annotation.*;
import com.embabel.agent.api.hitl.*;
@Agent(description = "Customer service agent with HITL interactions")
public class CustomerServiceAgent {
@Action(description = "Gather customer details")
public CustomerDetails gatherDetails(UserInput input) {
// Request customer details via form
FormBindingRequest<CustomerDetails> request =
WaitFor.formSubmission("Customer Information", CustomerDetails.class);
return request.await();
}
@Action(description = "Propose service plan")
public ServicePlan proposePlan(CustomerDetails details) {
// Analyze and propose a service plan
ServicePlan plan = analyzAndPropose(details);
// Request approval for additional features
ConfirmationRequest<String> premiumRequest =
WaitFor.confirmation(
"Premium Support",
"Include premium support package (+$50/month)?"
);
if (premiumRequest.await()) {
plan.addPremiumSupport();
}
return plan;
}
@AchievesGoal(
description = "Finalize customer service plan with approval",
tags = {"finalization", "approval"},
export = @Export(remote = true, local = true)
)
@Action(description = "Finalize plan")
public FinalizedPlan finalizePlan(ServicePlan plan) {
// Request final user confirmation
ConfirmationRequest<ServicePlan> request =
WaitFor.confirmation(plan, "Approve and activate this service plan?");
boolean approved = request.await();
if (approved) {
return activatePlan(plan);
} else {
throw new PlanRejectionException("User rejected the service plan");
}
}
private ServicePlan analyzAndPropose(CustomerDetails details) {
// Business logic
return new ServicePlan();
}
private FinalizedPlan activatePlan(ServicePlan plan) {
// Activation logic
return new FinalizedPlan(plan, true);
}
}import com.embabel.agent.api.annotation.*
@Agent(description = "Data processing agent with HITL interactions")
class DataProcessingAgent {
@Action(description = "Gather input data")
fun gatherInputData(request: ProcessRequest): InputData {
val formRequest = WaitFor.formSubmission(
"Provide Input Data",
InputData::class.java
)
return formRequest.await()
}
@Action(description = "Process data")
fun processData(input: InputData): ProcessedData {
val data = process(input)
// Confirm if uncertain results
if (data.confidence < 0.8) {
val confirmRequest = WaitFor.confirmation(
data,
"Processing confidence is ${data.confidence}. Proceed anyway?"
)
if (!confirmRequest.await()) {
throw ProcessingCancelledException("User cancelled low-confidence processing")
}
}
return data
}
@AchievesGoal(
description = "Generate and deliver final report",
tags = ["reporting", "delivery"],
export = Export(
name = "generate-report",
remote = true,
local = true,
startingInputTypes = [ProcessRequest::class]
)
)
@Action(description = "Generate report")
fun generateReport(processed: ProcessedData): Report {
val report = createReport(processed)
// Confirm delivery
val confirmRequest = WaitFor.confirmation(
report,
"Send report to stakeholders?"
)
if (confirmRequest.await()) {
deliverReport(report)
}
return report
}
private fun process(input: InputData) = ProcessedData(/* ... */)
private fun createReport(data: ProcessedData) = Report(/* ... */)
private fun deliverReport(report: Report) { /* ... */ }
}Request confirmation based on conditions.
@Agent(description = "Transaction processor")
public class TransactionAgent {
private static final double HIGH_VALUE_THRESHOLD = 10000.0;
@Action(description = "Process transaction")
public TransactionResult processTransaction(Transaction transaction) {
// Only require confirmation for high-value transactions
if (transaction.getAmount() > HIGH_VALUE_THRESHOLD) {
ConfirmationRequest<Transaction> request = WaitFor.confirmation(
transaction,
String.format(
"Confirm high-value transaction of $%.2f?",
transaction.getAmount()
)
);
if (!request.await()) {
return TransactionResult.cancelled();
}
}
// Process transaction
return paymentProcessor.process(transaction);
}
}@Agent(description = "Approval workflow")
class ApprovalAgent {
companion object {
const val AUTO_APPROVE_LIMIT = 1000.0
}
@Action(description = "Process approval request")
fun processApproval(request: ApprovalRequest): ApprovalResult {
// Auto-approve below threshold
if (request.amount < AUTO_APPROVE_LIMIT) {
return ApprovalResult.approved("Auto-approved")
}
// Request manual approval for high amounts
val confirmRequest = WaitFor.confirmation(
request,
"Approve amount of $${request.amount}?"
)
return if (confirmRequest.await()) {
ApprovalResult.approved("Manually approved")
} else {
ApprovalResult.rejected("User rejected")
}
}
}Handle forms with optional and required fields.
public class ProfileUpdateForm {
@NotBlank
private String name; // Required
private String bio; // Optional
@Email
private String email; // Optional but must be valid if provided
private String website; // Optional
// Getters and setters
}
@Agent(description = "Profile manager")
public class ProfileAgent {
@Action(description = "Update user profile")
public Profile updateProfile(User user) {
FormBindingRequest<ProfileUpdateForm> request =
WaitFor.formSubmission(
"Update Your Profile",
ProfileUpdateForm.class
);
ProfileUpdateForm form = request.await();
return profileService.update(user.getId(), form);
}
}data class ProfileUpdateForm(
@field:NotBlank
val name: String, // Required
val bio: String? = null, // Optional
@field:Email
val email: String? = null, // Optional but validated
val website: String? = null // Optional
)
@Agent(description = "Profile manager")
class ProfileAgent {
@Action(description = "Update user profile")
fun updateProfile(user: User): Profile {
val request = WaitFor.formSubmission(
"Update Your Profile",
ProfileUpdateForm::class.java
)
val form = request.await()
return profileService.update(user.id, form)
}
}Combine progress updates with HITL interactions.
@Agent(description = "Report generation agent")
public class ReportAgent {
@Action(description = "Generate comprehensive report")
public Report generateReport(
DataSet data,
@Provided ActionContext context
) {
// Update progress
context.updateProgress("Loading data...");
// Process data
context.updateProgress("Analyzing data...");
Analysis analysis = analyze(data);
// Request user input for report format
context.updateProgress("Waiting for user input...");
FormBindingRequest<ReportFormat> formatRequest =
WaitFor.formSubmission("Choose Report Format", ReportFormat.class);
ReportFormat format = formatRequest.await();
// Generate report
context.updateProgress("Generating " + format.getType() + " report...");
Report report = createReport(analysis, format);
// Confirm before sending
context.updateProgress("Waiting for confirmation...");
ConfirmationRequest<Report> confirmRequest =
WaitFor.confirmation(report, "Send report to recipients?");
if (confirmRequest.await()) {
context.updateProgress("Sending report...");
emailService.send(report);
}
context.updateProgress("Complete!");
return report;
}
}@Agent(description = "Batch processor with HITL")
class BatchProcessorAgent {
@Action(description = "Process batch with confirmation")
fun processBatch(
batch: Batch,
@Provided context: ActionContext
): BatchResult {
context.updateProgress("Processing ${batch.items.size} items")
val results = mutableListOf<ItemResult>()
for ((index, item) in batch.items.withIndex()) {
context.updateProgress("Processing item ${index + 1}/${batch.items.size}")
val result = processItem(item)
// Request confirmation for failed items
if (!result.success) {
context.updateProgress("Item ${index + 1} failed - waiting for decision")
val confirmRequest = WaitFor.confirmation(
result,
"Item ${index + 1} failed. Continue with remaining items?"
)
if (!confirmRequest.await()) {
throw ProcessingCancelledException("User cancelled batch processing")
}
}
results.add(result)
}
context.updateProgress("Batch processing complete")
return BatchResult(results)
}
private fun processItem(item: Item): ItemResult {
// Processing logic
return ItemResult()
}
}Implement custom awaitable operations.
import com.embabel.agent.api.hitl.Awaitable;
@Agent(description = "Custom interaction agent")
public class CustomInteractionAgent {
@Action(description = "Custom user interaction")
public Result customInteraction() {
// Custom awaitable implementation
Awaitable<CustomResult, ?> customAwaitable = createCustomAwaitable();
// Wrap for consistent handling
Awaitable<CustomResult, ?> wrapped = WaitFor.awaitable(customAwaitable);
// Use in action
CustomResult result = wrapped.await();
return processResult(result);
}
private Awaitable<CustomResult, ?> createCustomAwaitable() {
// Custom implementation
return new CustomAwaitable();
}
private class CustomAwaitable implements Awaitable<CustomResult, CustomAwaitable> {
@Override
public CustomResult await() {
// Custom await logic
return new CustomResult();
}
@Override
public boolean isComplete() {
return false;
}
@Override
public void cancel() {
// Cancellation logic
}
}
}import com.embabel.agent.api.hitl.Awaitable
@Agent(description = "Custom awaitable agent")
class CustomAwaitableAgent {
@Action(description = "Custom interaction")
fun customInteraction(): Result {
val customAwaitable = createCustomAwaitable()
val wrapped = WaitFor.awaitable(customAwaitable)
val result = wrapped.await()
return processResult(result)
}
private fun createCustomAwaitable(): Awaitable<CustomResult, *> {
return CustomAwaitable()
}
private class CustomAwaitable : Awaitable<CustomResult, CustomAwaitable> {
override fun await(): CustomResult {
// Custom await logic
return CustomResult()
}
override fun isComplete(): Boolean = false
override fun cancel() {
// Cancellation logic
}
}
}Use reactive patterns for non-blocking HITL.
import reactor.core.publisher.Mono;
@Agent(description = "Async HITL agent")
public class AsyncHitlAgent {
@Action(description = "Async confirmation")
public Mono<Result> asyncConfirmation(Input input) {
return Mono.fromCallable(() -> {
ConfirmationRequest<Input> request =
WaitFor.confirmation(input, "Proceed with this action?");
return request.await();
}).map(confirmed -> {
if (confirmed) {
return processInput(input);
} else {
return Result.cancelled();
}
});
}
}import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Agent(description = "Coroutine HITL agent")
class CoroutineHitlAgent {
@Action(description = "Async confirmation")
suspend fun asyncConfirmation(input: Input): Result {
return withContext(Dispatchers.IO) {
val request = WaitFor.confirmation(
input,
"Proceed with this action?"
)
val confirmed = request.await()
if (confirmed) {
processInput(input)
} else {
Result.cancelled()
}
}
}
}import com.embabel.agent.api.annotation.*;
import com.embabel.agent.api.hitl.*;
@Agent(
description = "Document approval workflow with HITL",
planner = PlannerType.GOAP
)
public class DocumentApprovalAgent {
private final DocumentService documentService;
private final ValidationService validationService;
private final ApprovalService approvalService;
public DocumentApprovalAgent(
DocumentService documentService,
ValidationService validationService,
ApprovalService approvalService
) {
this.documentService = documentService;
this.validationService = validationService;
this.approvalService = approvalService;
}
@Action(
description = "Upload document",
post = {"documentUploaded"},
outputBinding = "document"
)
public Document uploadDocument() {
// Request document metadata via form
FormBindingRequest<DocumentMetadata> metadataRequest =
WaitFor.formSubmission("Document Information", DocumentMetadata.class);
DocumentMetadata metadata = metadataRequest.await();
// Upload document
return documentService.upload(metadata);
}
@Action(
description = "Validate document",
pre = {"documentUploaded"},
post = {"documentValidated"},
outputBinding = "validationResult"
)
public ValidationResult validateDocument(
Document document,
@Provided ActionContext context
) {
context.updateProgress("Validating document: " + document.getName());
ValidationResult result = validationService.validate(document);
// If validation issues found, ask user to proceed
if (result.hasWarnings()) {
context.updateProgress("Validation warnings found");
ConfirmationRequest<ValidationResult> confirmRequest =
WaitFor.confirmation(
result,
"Document has validation warnings. Proceed anyway?"
);
if (!confirmRequest.await()) {
throw ValidationCancelledException("User cancelled due to warnings");
}
}
return result;
}
@Action(
description = "Request approvals",
pre = {"documentValidated"},
post = {"approvalsRequested"},
outputBinding = "approvalRequests"
)
public List<ApprovalRequest> requestApprovals(
Document document,
@Provided ActionContext context
) {
// Request approval configuration via form
context.updateProgress("Configuring approvals");
FormBindingRequest<ApprovalConfiguration> configRequest =
WaitFor.formSubmission(
"Approval Configuration",
ApprovalConfiguration.class
);
ApprovalConfiguration config = configRequest.await();
// Create approval requests
List<ApprovalRequest> requests = approvalService.createRequests(
document,
config
);
context.sendMessage(Message.info(
"Created " + requests.size() + " approval requests"
));
return requests;
}
@AchievesGoal(
description = "Complete document approval workflow",
tags = {"document", "approval", "workflow"},
export = @Export(remote = true, local = true),
value = 100.0
)
@Action(
description = "Finalize approval",
pre = {"approvalsRequested"},
canRerun = false
)
public ApprovedDocument finalizeApproval(
Document document,
List<ApprovalRequest> approvalRequests,
@Provided ActionContext context
) {
context.updateProgress("Waiting for all approvals...");
// Wait for all approvals
approvalService.waitForApprovals(approvalRequests);
context.updateProgress("All approvals received");
// Final confirmation before publishing
ConfirmationRequest<Document> publishRequest =
WaitFor.confirmation(
document,
"All approvals received. Publish document?"
);
if (publishRequest.await()) {
ApprovedDocument approved = documentService.publish(document);
context.sendMessage(Message.info(
"Document published: " + approved.getPublishedUrl()
));
return approved;
} else {
throw PublishCancelledException("User cancelled publishing");
}
}
}
// Form data classes
public class DocumentMetadata {
@NotBlank
private String title;
@NotBlank
private String author;
private String description;
@NotBlank
private String category;
// Getters and setters
}
public class ApprovalConfiguration {
@NotEmpty
private List<String> approverEmails;
@Min(1)
private int requiredApprovals;
private boolean requireAllApprovals;
// Getters and setters
}import com.embabel.agent.api.annotation.*
import javax.validation.constraints.*
@Agent(
description = "Expense approval workflow with HITL",
planner = PlannerType.GOAP
)
class ExpenseApprovalAgent(
private val expenseService: ExpenseService,
private val policyChecker: PolicyChecker,
private val approvalService: ApprovalService
) {
@Action(
description = "Submit expense",
post = ["expenseSubmitted"],
outputBinding = "expense"
)
fun submitExpense(): Expense {
// Request expense details via form
val request = WaitFor.formSubmission(
"Submit Expense",
ExpenseForm::class.java
)
val form = request.await()
return expenseService.create(form)
}
@Action(
description = "Check policy compliance",
pre = ["expenseSubmitted"],
post = ["policyChecked"],
outputBinding = "policyResult"
)
fun checkPolicy(
expense: Expense,
@Provided context: ActionContext
): PolicyResult {
context.updateProgress("Checking policy compliance")
val result = policyChecker.check(expense)
// If policy violations, ask user to justify
if (result.hasViolations()) {
context.updateProgress("Policy violations detected")
val justificationRequest = WaitFor.formSubmission(
"Provide Justification",
JustificationForm::class.java
)
val justification = justificationRequest.await()
result.addJustification(justification)
}
return result
}
@Action(
description = "Request manager approval",
pre = ["policyChecked"],
post = ["approvalRequested"],
outputBinding = "approvalStatus"
)
fun requestApproval(
expense: Expense,
policyResult: PolicyResult,
@Provided context: ActionContext
): ApprovalStatus {
// Determine approval routing
val routing = if (expense.amount > 1000.0) {
// High amount - request approval routing config
context.updateProgress("High amount expense - configure approval")
val routingRequest = WaitFor.formSubmission(
"Approval Routing",
ApprovalRoutingForm::class.java
)
routingRequest.await()
} else {
// Standard routing
ApprovalRoutingForm.standard()
}
return approvalService.requestApproval(expense, routing)
}
@AchievesGoal(
description = "Complete expense approval and reimbursement",
tags = ["expense", "approval", "reimbursement"],
export = Export(remote = true, local = true),
value = 100.0
)
@Action(
description = "Process reimbursement",
pre = ["approvalRequested"],
canRerun = false
)
fun processReimbursement(
expense: Expense,
approvalStatus: ApprovalStatus,
@Provided context: ActionContext
): Reimbursement {
context.updateProgress("Waiting for approval")
// Wait for approval decision
approvalService.waitForDecision(approvalStatus)
if (!approvalStatus.isApproved) {
throw ExpenseRejectedException("Expense rejected by approver")
}
context.updateProgress("Expense approved")
// Confirm reimbursement method
val methodRequest = WaitFor.formSubmission(
"Reimbursement Method",
ReimbursementMethodForm::class.java
)
val method = methodRequest.await()
// Process reimbursement
val reimbursement = expenseService.reimburse(expense, method)
context.sendMessage(Message.info(
"Reimbursement processed: $${reimbursement.amount}"
))
return reimbursement
}
}
// Form data classes
data class ExpenseForm(
@field:NotBlank
val description: String,
@field:Min(0)
val amount: Double,
@field:NotBlank
val category: String,
@field:NotNull
val date: LocalDate,
val receiptUrl: String? = null
)
data class JustificationForm(
@field:NotBlank
@field:Size(min = 50)
val justification: String,
val supportingDocuments: List<String> = emptyList()
)
data class ApprovalRoutingForm(
@field:NotEmpty
val approvers: List<String>,
val requireAllApprovals: Boolean = false
) {
companion object {
fun standard() = ApprovalRoutingForm(
approvers = listOf("manager"),
requireAllApprovals = false
)
}
}
data class ReimbursementMethodForm(
@field:NotBlank
val method: String, // "direct_deposit", "check", "payroll"
val accountNumber: String? = null
)tessl i tessl/maven-com-embabel-agent--embabel-agent-starter@0.3.1docs