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 coordinate multiple agents using supervisor, sequence, parallel, loop, conditional, and planner patterns.
A supervisor agent coordinates multiple sub-agents, delegating tasks and aggregating responses according to a specified strategy.
/**
* Defines a supervisor agent that coordinates multiple sub-agents
*
* @param description - Description of the supervisor
* @param outputKey - Key for output storage (optional)
* @param subAgents - Array of sub-agent classes to coordinate
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface SupervisorAgent {
String description() default "";
String outputKey() default "";
Class<?>[] subAgents();
}Usage Example:
// Define sub-agents with specific expertise
public interface WithdrawAgent {
@SystemMessage("You are a banker that can only withdraw USD from a user account.")
@UserMessage("Withdraw {{amountInUSD}} USD from {{withdrawUser}}'s account and return the new balance.")
@Agent("A banker that withdraws USD from an account")
String withdraw(@V("withdrawUser") String user, @V("amountInUSD") Double amount);
}
public interface CreditAgent {
@SystemMessage("You are a banker that can only credit USD to a user account.")
@UserMessage("Credit {{amountInUSD}} USD to {{creditUser}}'s account and return the new balance.")
@Agent("A banker that credits USD to an account")
String credit(@V("creditUser") String user, @V("amountInUSD") Double amount);
}
public interface ExchangeAgent {
@UserMessage("""
You are an operator exchanging money in different currencies.
Use the tool to exchange {{amount}} {{originalCurrency}} into {{targetCurrency}}
returning only the final amount provided by the tool and nothing else.
""")
@Agent(outputKey = "exchange", description = "Converts money between currencies")
Double exchange(
@V("originalCurrency") String originalCurrency,
@V("amount") Double amount,
@V("targetCurrency") String targetCurrency
);
}
// Supervisor coordinates all banking operations
public interface BankingSupervisor {
@SupervisorAgent(
subAgents = { WithdrawAgent.class, CreditAgent.class, ExchangeAgent.class }
)
String handleBankingRequest(@V("request") String request);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
BankingSupervisor supervisor;
String result = supervisor.handleBankingRequest(
"Transfer $500 from Alice's account to Bob's account"
);Customize the initial request sent to the supervisor using @SupervisorRequest.
/**
* Provides the initial request for a supervisor agent
*
* @return The request string to send to the supervisor
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface SupervisorRequest {
}
// Applied to a static method
@SupervisorRequest
static String buildRequest(@V("param") Type param) {
// Build and return custom request
}Usage Example:
public interface StoryCreationSupervisor {
@SupervisorAgent(
outputKey = "story",
subAgents = { CreativeWriter.class, StyleEditor.class }
)
ResultWithAgenticScope<String> createStory(@V("topic") String topic, @V("style") String style);
@SupervisorRequest
static String buildRequest(@V("topic") String topic, @V("style") String style) {
return String.format(
"Write a creative story about %s in %s style, then refine it for quality",
topic, style
);
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}A sequence agent executes sub-agents in sequential order, passing outputs through the agentic scope.
/**
* Defines an agent that executes sub-agents in sequential order
*
* @param description - Description of the sequence (optional)
* @param outputKey - Key for final output (required)
* @param subAgents - Array of sub-agent classes to execute in order
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface SequenceAgent {
String description() default "";
String outputKey();
Class<?>[] subAgents();
}Usage Example:
// Define sequential steps
public interface ResearchAgent {
@UserMessage("Research information about: {{topic}}")
@Agent(description = "Researches a topic", outputKey = "research")
String research(@V("topic") String topic);
}
public interface AnalysisAgent {
@UserMessage("Analyze the following research and extract key insights: {{research}}")
@Agent(description = "Analyzes research", outputKey = "analysis")
String analyze(@V("research") String research);
}
public interface ReportAgent {
@UserMessage("""
Create a professional report based on the following analysis:
{{analysis}}
Format: {{format}}
""")
@Agent(description = "Generates a report", outputKey = "report")
String generateReport(@V("analysis") String analysis, @V("format") String format);
}
// Compose them in a sequence
public interface ReportGenerator {
@SequenceAgent(
description = "Generates a research report",
outputKey = "report",
subAgents = { ResearchAgent.class, AnalysisAgent.class, ReportAgent.class }
)
ResultWithAgenticScope<String> generateReport(@V("topic") String topic, @V("format") String format);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
ReportGenerator generator;
ResultWithAgenticScope<String> result = generator.generateReport("AI Safety", "PDF");
String report = result.result();
String research = result.agenticScope().readState("research");
String analysis = result.agenticScope().readState("analysis");A parallel agent executes sub-agents concurrently, aggregating their results.
/**
* Defines an agent that executes sub-agents in parallel
*
* @param description - Description of the parallel execution (optional)
* @param outputKey - Key for output storage (optional)
* @param subAgents - Array of sub-agent classes to execute in parallel
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ParallelAgent {
String description() default "";
String outputKey() default "";
Class<?>[] subAgents();
}Usage Example:
// Define independent analysis agents
public interface TechnicalAnalyzer {
@UserMessage("Analyze technical aspects of: {{product}}")
@Agent(description = "Technical analysis", outputKey = "technicalAnalysis")
String analyzeTechnical(@V("product") String product);
}
public interface MarketAnalyzer {
@UserMessage("Analyze market potential of: {{product}}")
@Agent(description = "Market analysis", outputKey = "marketAnalysis")
String analyzeMarket(@V("product") String product);
}
public interface CompetitorAnalyzer {
@UserMessage("Analyze competitors for: {{product}}")
@Agent(description = "Competitor analysis", outputKey = "competitorAnalysis")
String analyzeCompetitors(@V("product") String product);
}
// Run analyses in parallel
public interface ProductAnalyzer {
@ParallelAgent(
description = "Comprehensive product analysis",
outputKey = "analyses",
subAgents = {
TechnicalAnalyzer.class,
MarketAnalyzer.class,
CompetitorAnalyzer.class
}
)
ResultWithAgenticScope<String> analyzeProduct(@V("product") String product);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
ProductAnalyzer analyzer;
ResultWithAgenticScope<String> result = analyzer.analyzeProduct("Smart Watch X");
String technical = result.agenticScope().readState("technicalAnalysis");
String market = result.agenticScope().readState("marketAnalysis");
String competitor = result.agenticScope().readState("competitorAnalysis");A loop agent repeatedly executes sub-agents until an exit condition is met or max iterations is reached.
/**
* Defines an agent that repeatedly executes sub-agents until condition is met
*
* @param description - Description of the loop (optional)
* @param outputKey - Key for output storage (required)
* @param maxIterations - Maximum number of loop iterations
* @param subAgents - Array of sub-agent classes to execute in each iteration
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface LoopAgent {
String description() default "";
String outputKey();
int maxIterations();
Class<?>[] subAgents();
}Usage Example:
// Define evaluation and refinement agents
public interface QualityScorer {
@UserMessage("""
Score the quality of the following content on a scale of 0.0 to 1.0.
Consider clarity, accuracy, and completeness.
Respond with only the numeric score.
Content: {{content}}
""")
@Agent(description = "Scores content quality", outputKey = "score")
double scoreQuality(@V("content") String content);
}
public interface ContentRefiner {
@UserMessage("""
Improve the following content to address quality issues.
Focus on clarity, accuracy, and completeness.
Content: {{content}}
""")
@Agent(description = "Refines content", outputKey = "content")
String refineContent(@V("content") String content);
}
// Loop until quality threshold is met
public interface ContentPolisher {
@LoopAgent(
description = "Refines content until quality threshold is met",
outputKey = "content",
maxIterations = 5,
subAgents = { QualityScorer.class, ContentRefiner.class }
)
ResultWithAgenticScope<String> polishContent(@V("content") String initialContent);
@ExitCondition
static boolean isQualitySufficient(@V("score") double score) {
return score >= 0.8;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
ContentPolisher polisher;
ResultWithAgenticScope<String> result = polisher.polishContent("Initial draft content...");
String polishedContent = result.result();
double finalScore = result.agenticScope().readState("score", 0.0);A conditional agent executes sub-agents based on activation conditions.
/**
* Defines an agent that conditionally executes sub-agents based on activation conditions
*
* @param description - Description (optional)
* @param outputKey - Key for output storage (optional)
* @param subAgents - Array of sub-agent classes with activation conditions
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ConditionalAgent {
String description() default "";
String outputKey() default "";
Class<?>[] subAgents();
}Usage Example:
public enum RequestCategory {
MEDICAL, TECHNICAL, LEGAL, UNKNOWN
}
// Categorization agent
public interface RequestCategorizer {
@UserMessage("""
Analyze the following user request and categorize it as 'MEDICAL', 'TECHNICAL', or 'LEGAL'.
If it doesn't fit, respond 'UNKNOWN'.
Respond with only the category name.
Request: {{request}}
""")
@Agent(description = "Categorizes requests", outputKey = "category")
RequestCategory categorize(@V("request") String request);
}
// Expert agents
public interface MedicalExpert {
@SystemMessage("You are a medical expert.")
@UserMessage("Provide medical advice for: {{request}}")
@Agent(description = "Medical expert", outputKey = "response")
String provideMedicalAdvice(@V("request") String request);
}
public interface TechnicalExpert {
@SystemMessage("You are a technical expert.")
@UserMessage("Provide technical support for: {{request}}")
@Agent(description = "Technical expert", outputKey = "response")
String provideTechnicalSupport(@V("request") String request);
}
public interface LegalExpert {
@SystemMessage("You are a legal expert.")
@UserMessage("Provide legal guidance for: {{request}}")
@Agent(description = "Legal expert", outputKey = "response")
String provideLegalGuidance(@V("request") String request);
}
// Conditional routing
public interface ExpertRouter {
@SequenceAgent(
outputKey = "response",
subAgents = { RequestCategorizer.class, ConditionalExpertAgent.class }
)
ResultWithAgenticScope<String> routeToExpert(@V("request") String request);
}
public interface ConditionalExpertAgent {
@ConditionalAgent(
outputKey = "response",
subAgents = { MedicalExpert.class, TechnicalExpert.class, LegalExpert.class }
)
String consultExpert(@V("request") String request);
@ActivationCondition(MedicalExpert.class)
static boolean activateMedical(@V("category") RequestCategory category) {
return category == RequestCategory.MEDICAL;
}
@ActivationCondition(TechnicalExpert.class)
static boolean activateTechnical(@V("category") RequestCategory category) {
return category == RequestCategory.TECHNICAL;
}
@ActivationCondition(LegalExpert.class)
static boolean activateLegal(@V("category") RequestCategory category) {
return category == RequestCategory.LEGAL;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
ExpertRouter router;
ResultWithAgenticScope<String> result = router.routeToExpert("I broke my leg, what should I do?");
String response = result.result();
RequestCategory category = result.agenticScope().readState("category");A planner agent creates and executes plans for complex workflows, dynamically orchestrating sub-agents.
/**
* Defines a planner agent that creates and executes plans for complex workflows
*
* @param description - Description (optional)
* @param outputKey - Key for output storage (optional)
* @param subAgents - Array of sub-agent classes available to the planner
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface PlannerAgent {
String description() default "";
String outputKey() default "";
Class<?>[] subAgents();
}Usage Example:
// Define task-specific agents
public interface DataGatherer {
@UserMessage("Gather data about: {{topic}}")
@Agent(description = "Gathers relevant data", outputKey = "data")
String gatherData(@V("topic") String topic);
}
public interface DataAnalyzer {
@UserMessage("Analyze the following data: {{data}}")
@Agent(description = "Analyzes data", outputKey = "analysis")
String analyzeData(@V("data") String data);
}
public interface ReportWriter {
@UserMessage("Write a report based on: {{analysis}}")
@Agent(description = "Writes reports", outputKey = "report")
String writeReport(@V("analysis") String analysis);
}
public interface DataVisualizer {
@UserMessage("Create visualizations for: {{data}}")
@Agent(description = "Creates visualizations", outputKey = "visualization")
String createVisualization(@V("data") String data);
}
// Planner coordinates complex workflows
public interface TaskPlanner {
@PlannerAgent(
description = "Plans and executes complex data analysis tasks",
outputKey = "result",
subAgents = {
DataGatherer.class,
DataAnalyzer.class,
ReportWriter.class,
DataVisualizer.class
}
)
ResultWithAgenticScope<String> executeTask(@V("task") String task);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage - planner will determine which agents to use and in what order
@Inject
TaskPlanner planner;
ResultWithAgenticScope<String> result = planner.executeTask(
"Analyze sales trends for Q4 2024 and create a visual dashboard"
);
String finalResult = result.result();An A2A client agent facilitates agent-to-agent communication following the A2A (Agent-to-Agent) protocol. This enables distributed agent systems where agents can invoke remote agents hosted on different servers or services.
/**
* Defines an Agent-to-Agent (A2A) client agent
* Enables communication with remote agents via A2A protocol
*
* @param description - Description of the agent (optional)
* @param outputKey - Key for output storage (optional)
* @param a2aServerUrl - URL of the A2A server hosting the remote agent (required)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface A2AClientAgent {
String description() default "";
String outputKey() default "";
String a2aServerUrl();
}Usage Example:
public interface RemoteStoryGenerator {
@A2AClientAgent(
a2aServerUrl = "http://story-service.example.com:8080",
description = "Communicates with remote story generation service",
outputKey = "story"
)
String generateStory(@V("topic") String topic, @V("style") String style);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage in a multi-agent workflow
public interface DistributedContentCreator {
@SequenceAgent(
outputKey = "finalContent",
subAgents = { RemoteStoryGenerator.class, LocalEditorAgent.class }
)
ResultWithAgenticScope<String> createContent(@V("topic") String topic, @V("style") String style);
}
// Usage
@Inject
RemoteStoryGenerator remoteAgent;
String story = remoteAgent.generateStory("space exploration", "sci-fi");Complex workflows can combine multiple orchestration patterns.
Usage Example:
// Combine sequence, parallel, and loop patterns
public interface ComplexWorkflow {
@SequenceAgent(
outputKey = "finalResult",
subAgents = {
InitialProcessor.class, // Step 1: Initial processing
ParallelAnalysis.class, // Step 2: Parallel analyses
RefinementLoop.class, // Step 3: Iterative refinement
FinalSynthesis.class // Step 4: Final synthesis
}
)
ResultWithAgenticScope<String> executeWorkflow(@V("input") String input);
}
public interface ParallelAnalysis {
@ParallelAgent(
outputKey = "analyses",
subAgents = { TechAnalyzer.class, BusinessAnalyzer.class, RiskAnalyzer.class }
)
String performAnalyses(@V("processedInput") String processedInput);
}
public interface RefinementLoop {
@LoopAgent(
description = "Refine results iteratively",
outputKey = "refinedResult",
maxIterations = 3,
subAgents = { Evaluator.class, Refiner.class }
)
String refineResults(@V("analyses") String analyses);
@ExitCondition
static boolean isRefined(@V("quality") double quality) {
return quality >= 0.9;
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-agentic@1.7.0