Quarkus extension that integrates LangChain4j's agentic capabilities, enabling developers to build AI agent-based applications using declarative patterns with support for multiple agent types, agent-to-agent communication, and CDI integration.
This document describes how to control agent execution with activation conditions, exit conditions, error handlers, and human-in-the-loop integration.
Defines a condition for activating a specific sub-agent in conditional execution.
/**
* Defines activation condition for a sub-agent in @ConditionalAgent
* Method must be static and return boolean
*
* @param subAgentClass - The sub-agent class this condition applies to
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ActivationCondition {
Class<?> value();
}
// Usage
@ActivationCondition(MySubAgent.class)
static boolean shouldActivate(@V("condition") ConditionType condition) {
return condition == ConditionType.EXPECTED;
}Usage Example:
public enum Priority {
LOW, MEDIUM, HIGH, CRITICAL
}
public interface LowPriorityAgent {
@Agent(description = "Handles low priority tasks", outputKey = "result")
String handle(@V("task") String task);
}
public interface HighPriorityAgent {
@Agent(description = "Handles high priority tasks", outputKey = "result")
String handle(@V("task") String task);
}
public interface CriticalPriorityAgent {
@Agent(description = "Handles critical tasks", outputKey = "result")
String handle(@V("task") String task);
}
public interface PriorityRouter {
@ConditionalAgent(
outputKey = "result",
subAgents = { LowPriorityAgent.class, HighPriorityAgent.class, CriticalPriorityAgent.class }
)
String route(@V("task") String task, @V("priority") Priority priority);
@ActivationCondition(LowPriorityAgent.class)
static boolean activateLow(@V("priority") Priority priority) {
return priority == Priority.LOW || priority == Priority.MEDIUM;
}
@ActivationCondition(HighPriorityAgent.class)
static boolean activateHigh(@V("priority") Priority priority) {
return priority == Priority.HIGH;
}
@ActivationCondition(CriticalPriorityAgent.class)
static boolean activateCritical(@V("priority") Priority priority) {
return priority == Priority.CRITICAL;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}Defines an exit condition for loop agents to determine when to stop iteration.
/**
* Defines exit condition for @LoopAgent
* Method must be static and return boolean
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ExitCondition {
}
// Usage
@ExitCondition
static boolean shouldExit(@V("metric") double metric) {
return metric >= threshold;
}Usage Example:
public interface QualityChecker {
@UserMessage("Rate the quality of this content (0.0-1.0): {{content}}")
@Agent(description = "Checks content quality", outputKey = "qualityScore")
double checkQuality(@V("content") String content);
}
public interface ContentImprover {
@UserMessage("Improve the following content: {{content}}")
@Agent(description = "Improves content", outputKey = "content")
String improveContent(@V("content") String content);
}
public interface IterativeImprover {
@LoopAgent(
description = "Iteratively improves content until quality threshold is met",
outputKey = "content",
maxIterations = 10,
subAgents = { QualityChecker.class, ContentImprover.class }
)
ResultWithAgenticScope<String> improve(@V("content") String initialContent);
@ExitCondition
static boolean isQualitySufficient(@V("qualityScore") double score) {
return score >= 0.85;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
IterativeImprover improver;
ResultWithAgenticScope<String> result = improver.improve("Initial draft...");
String improvedContent = result.result();
double finalScore = result.agenticScope().readState("qualityScore", 0.0);
int iterations = result.agenticScope().readState("iterations", 0);Handles errors during agent execution and determines recovery strategy.
/**
* Handles errors during agent execution
* Method must be static
* Parameter type must be ErrorContext
* Return type must be ErrorRecoveryResult
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ErrorHandler {
}
// Usage
@ErrorHandler
static ErrorRecoveryResult handleError(ErrorContext context) {
// Inspect error and determine recovery strategy
if (shouldRetry) {
return ErrorRecoveryResult.retry();
}
return ErrorRecoveryResult.throwException();
}Usage Example:
import dev.langchain4j.agentic.agent.ErrorContext;
import dev.langchain4j.agentic.agent.ErrorRecoveryResult;
import dev.langchain4j.agentic.agent.MissingArgumentException;
public interface ResilientAgent {
@Agent(description = "Processes data with error recovery", outputKey = "result")
String processData(@V("data") String data, @V("options") String options);
@ErrorHandler
static ErrorRecoveryResult handleError(ErrorContext context) {
System.err.println("Error in agent: " + context.agentName());
System.err.println("Exception: " + context.exception().getMessage());
// Handle missing argument by providing default
if (context.exception() instanceof MissingArgumentException mEx) {
String argName = mEx.argumentName();
if ("options".equals(argName)) {
context.agenticScope().writeState("options", "default");
return ErrorRecoveryResult.retry();
}
}
// Handle timeout by retrying once
if (context.exception() instanceof TimeoutException) {
Integer retryCount = context.agenticScope().readState("retryCount", 0);
if (retryCount < 1) {
context.agenticScope().writeState("retryCount", retryCount + 1);
return ErrorRecoveryResult.retry();
}
}
// For other errors, throw exception
return ErrorRecoveryResult.throwException();
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}Marks a point where human intervention or approval is required.
/**
* Marks a point requiring human intervention
* Method must be static with void return type
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface HumanInTheLoop {
}
// Usage
@HumanInTheLoop
static void requestHumanReview(@V("data") DataType data) {
// Notify human, log request, etc.
}Usage Example:
public interface ReviewWorkflow {
@Agent(description = "Generates content for review", outputKey = "draft")
String generateDraft(@V("topic") String topic);
@HumanInTheLoop
static void requestHumanReview(@V("draft") String draft, @V("topic") String topic) {
System.out.println("=== Human Review Required ===");
System.out.println("Topic: " + topic);
System.out.println("Draft: " + draft);
System.out.println("Please review and approve/reject");
// In production: send to review queue, notify reviewer, etc.
}
@HumanInTheLoopResponseSupplier
static String getReviewResponse() {
// Fetch human response from queue, UI, etc.
// For demo purposes, auto-approve
return "approved";
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}Marks a method as handling agent output for custom processing.
/**
* Marks a method as an output handler
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Output {
}
// Usage
@Output
void processOutput(@V("result") String result) {
// Custom output handling
}Usage Example:
public interface DataPipeline {
@Agent(description = "Processes data", outputKey = "processed")
String process(@V("input") String input);
@Output
void handleOutput(@V("processed") String processed, @V("input") String original) {
System.out.println("Original: " + original);
System.out.println("Processed: " + processed);
// Additional processing: save to database, send notification, etc.
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}Customizes the initial request sent to a supervisor agent.
/**
* Provides the initial request for a supervisor agent
* Method must be static and return String
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface SupervisorRequest {
}
// Usage
@SupervisorRequest
static String buildRequest(@V("param") Type param) {
return "Custom request: " + param;
}Usage Example:
public interface ContentCreationSupervisor {
@SupervisorAgent(
outputKey = "content",
responseStrategy = SupervisorResponseStrategy.SUMMARY,
subAgents = { Researcher.class, Writer.class, Editor.class }
)
ResultWithAgenticScope<String> createContent(
@V("topic") String topic,
@V("audience") String audience,
@V("tone") String tone
);
@SupervisorRequest
static String buildRequest(
@V("topic") String topic,
@V("audience") String audience,
@V("tone") String tone
) {
return String.format(
"Create comprehensive content about '%s' for %s audience with %s tone. " +
"Ensure thorough research, engaging writing, and careful editing.",
topic, audience, tone
);
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}Combining multiple lifecycle and control mechanisms:
public interface ComplexWorkflowAgent {
@SequenceAgent(
outputKey = "finalResult",
subAgents = { DataCollector.class, ConditionalProcessor.class, IterativeRefiner.class }
)
ResultWithAgenticScope<String> executeWorkflow(
@V("input") String input,
@V("mode") String mode
);
@ErrorHandler
static ErrorRecoveryResult handleError(ErrorContext context) {
// Global error handling for the workflow
if (context.exception() instanceof ValidationException) {
// Log and provide default values
context.agenticScope().writeState("validated", false);
return ErrorRecoveryResult.retry();
}
return ErrorRecoveryResult.throwException();
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
public interface ConditionalProcessor {
@ConditionalAgent(
outputKey = "processed",
subAgents = { FastProcessor.class, ThoroughProcessor.class }
)
String process(@V("data") String data, @V("mode") String mode);
@ActivationCondition(FastProcessor.class)
static boolean useFastProcessor(@V("mode") String mode) {
return "fast".equalsIgnoreCase(mode);
}
@ActivationCondition(ThoroughProcessor.class)
static boolean useThoroughProcessor(@V("mode") String mode) {
return "thorough".equalsIgnoreCase(mode);
}
}
public interface IterativeRefiner {
@LoopAgent(
description = "Refines output iteratively",
outputKey = "refined",
maxIterations = 5,
subAgents = { QualityEvaluator.class, Refiner.class }
)
String refine(@V("processed") String processed);
@ExitCondition
static boolean isRefinementComplete(@V("quality") double quality) {
return quality >= 0.9;
}
@HumanInTheLoop
static void reviewIfNeeded(@V("refined") String refined, @V("quality") double quality) {
if (quality < 0.95) {
System.out.println("Quality below optimal, human review recommended");
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-agentic@1.7.0