Step 04 - Supervisor Pattern for Autonomous Agentic Orchestration
Supervisor Pattern for Autonomous Agentic Orchestration
In the previous step, you created nested workflows that combined sequential, parallel, and conditional patterns to build sophisticated multi-level orchestration.
That design was intentionally concrete. Step 03 introduced separate feedback agents and a straightforward @ParallelAgent workflow so you could clearly see how multiple specialized agents collaborate inside a larger workflow.
In this step, the business problem evolves. We are still building on the same orchestration ideas, but the addition of a third feedback dimension creates a good opportunity to refactor the architecture. Rather than adding yet another nearly identical feedback agent and keeping the duplication, you’ll move to a more flexible pattern: a single parameterized feedback agent executed multiple times in parallel by a supervisor-driven workflow.
This step therefore introduces two ideas at once. First, you’ll learn the Supervisor Pattern, where an AI agent autonomously decides which downstream agents to invoke. Second, you’ll see how to evolve a workflow from multiple concrete agents to a reusable, task-driven design using @ParallelMapperAgent.
New Requirement from Miles of Smiles Management: Intelligent Disposition Decisions
The Miles of Smiles management team has identified a new challenge: they need to make intelligent decisions about vehicle disposition when cars return with severe damage.
The system needs to:
- Detect severe damage that might make a car uneconomical to repair
- Estimate vehicle value to inform disposition decisions
- Decide disposition strategy (SCRAP, SELL, DONATE, or KEEP) based on:
- Car value
- Age of the vehicle
- Severity of damage
- Repair cost estimates
- Let an AI supervisor orchestrate the entire decision-making process
What You’ll Learn
In this step, you will:
- Understand the Supervisor Pattern and when to use it
- Implement a supervisor agent using the
@SupervisorAgentannotation - Refactor three similar feedback analyzers into a single parameterized
FeedbackAnalysisAgent - Model feedback work as reusable
FeedbackTaskinstances - Use
@ParallelMapperAgentto invoke the same agent multiple times in parallel - Use an
@Outputmethod to transform raw workflow results into structured data - Build a
PricingAgentto estimate vehicle market values - Create a
DispositionAgentto make SCRAP/SELL/DONATE/KEEP decisions - See how supervisors provide autonomous, adaptive orchestration
Understanding the Supervisor Pattern
What is a Supervisor Agent?
A supervisor agent is an AI agent that:
- Autonomously coordinates other (sub-)agents
- Makes runtime decisions about which agents to invoke
- Adapts to context using business rules and current conditions
- Provides autonomous orchestration without hardcoded routing logic
Supervisor vs. Conditional Workflows
| Aspect | Conditional Workflow | Supervisor Agent |
|---|---|---|
| Decision Logic | Hardcoded conditions | AI-driven decisions |
| Flexibility | Fixed rules | Adapts to context |
| Complexity | Simple boolean checks | Complex reasoning |
| Maintenance | Update code for changes | Update prompts/context |
When to Use Supervisors
Use supervisor agents when you need:
- Context-aware routing where decisions are based on multiple factors that are hard to predict
- Business rule flexibility that is easier to adjust through instructions than code changes
- Complex orchestration with multiple agents that have interdependencies
From Step 03’s Concrete Agents to Step 04’s Parameterized Design
Step 03 deliberately used concrete feedback agents because that made the composition pattern easy to understand. You had one agent for cleaning feedback and one for maintenance feedback, and the workflow simply ran both in parallel. That was the right teaching choice for introducing workflow composition.
Step 04 adds a third perspective: disposition analysis. At that point, the similarity between the feedback agents becomes hard to ignore. Each one reads the same car information and the same feedback sources. The main difference is the instructions it follows and the output it produces.
Instead of creating one more near-duplicate agent, this step refactors the design so the variability becomes explicit data. The analysis logic stays in one place, and the workflow passes in a task object that tells the agent what kind of feedback it is analyzing.
This is a useful architectural progression to understand. You start with concrete, easy-to-read building blocks when learning the model. Once the pattern is familiar, you can refactor toward something more reusable and maintainable.
What is Being Added?
We’re going to enhance our car management system with:
- a
FleetSupervisorAgentthat orchestrates action agents autonomously - a unified
FeedbackAnalysisAgentthat handles cleaning, maintenance, and disposition analysis - a
FeedbackTaskmodel that defines each analysis task - a
FeedbackAnalysisWorkflowthat runs the same agent multiple times in parallel - a
FeedbackAnalysisResultsrecord that holds the three analysis results - a
PricingAgentthat estimates vehicle market value - a
DispositionAgentthat decides whether to SCRAP, SELL, DONATE, or KEEP a vehicle - an updated workflow with two distinct phases: parallel analysis followed by supervisor-driven action orchestration
The New Architecture
graph TB
Start([Car Return]) --> A[CarProcessingWorkflow<br/>Sequential]
A --> B[Step 1: FeedbackAnalysisWorkflow<br/>Parallel Mapper]
B --> B1[FeedbackTask.cleaning()]
B --> B2[FeedbackTask.maintenance()]
B --> B3[FeedbackTask.disposition()<br/>NEW]
B1 --> BA[FeedbackAnalysisAgent]
B2 --> BA
B3 --> BA
BA --> BEnd[FeedbackAnalysisResults]
BEnd --> C[Step 2: FleetSupervisorAgent<br/>Autonomous Orchestration]
C --> C1{AI Supervisor<br/>Analyzes Results}
C1 -->|Severe Damage| C2[PricingAgent<br/>Estimate Value]
C2 --> C3[DispositionAgent<br/>SCRAP/SELL/DONATE/KEEP]
C1 -->|Repairable| C4[MaintenanceAgent]
C1 -->|Minor Issues| C5[CleaningAgent]
C3 --> CEnd[Supervisor Decision]
C4 --> CEnd
C5 --> CEnd
CEnd --> D[Step 3: CarConditionFeedbackAgent<br/>Final Summary]
D --> End([Updated Car with Status])
style A fill:#90EE90
style B fill:#87CEEB
style C fill:#FFB6C1
style D fill:#90EE90
style C1 fill:#FFA07A
style B3 fill:#FFD700
style C2 fill:#FFD700
style C3 fill:#FFD700
style Start fill:#E8E8E8
style End fill:#E8E8E8
The key innovation is that the system still performs three analyses in parallel, but it no longer needs three separate feedback agent types to do that work. Instead, the workflow builds a list of tasks and executes the same agent once per task.
That gives you a cleaner design with less duplication, while preserving the same behavior and the same parallelism.
Implementing the Supervisor Pattern
Let’s build the new autonomous dispositioning system step by step.
Prerequisites
Before starting:
- Completed Step 03 (or have the
section-2/step-03code available) - Application from Step 03 is stopped (Ctrl+C)
If you want to continue building on your Step 03 code, copy the updated UI files from step-04:
cd section-2/step-03
cp ../step-04/src/main/resources/META-INF/resources/css/styles.css ./src/main/resources/META-INF/resources/css/styles.css
cp ../step-04/src/main/resources/META-INF/resources/js/app.js ./src/main/resources/META-INF/resources/js/app.js
cp ../step-04/src/main/resources/META-INF/resources/index.html ./src/main/resources/META-INF/resources/index.html
cd section-2\step-03
copy ..\step-04\src\main\resources\META-INF\resources\css\styles.css .\src\main\resources\META-INF\resources\css\styles.css
copy ..\step-04\src\main\resources\META-INF\resources\js\app.js .\src\main\resources\META-INF\resources\js\app.js
copy ..\step-04\src\main\resources\META-INF\resources\index.html .\src\main\resources\META-INF\resources\index.html
Create the Feedback Task Model
The first step in the refactoring is to make the analysis type explicit. Instead of encoding “cleaning”, “maintenance”, or “disposition” in three separate agent interfaces, we represent that variation with a task model.
Create src/main/java/com/carmanagement/model/FeedbackTask.java:
package com.carmanagement.model;
/**
* Record representing a feedback analysis task with its configuration.
* Contains the type of feedback and system instructions for the analysis.
*/
public record FeedbackTask(
FeedbackType feedbackType,
String systemInstructions) {
/**
* Factory method for creating a cleaning feedback task.
*/
public static FeedbackTask cleaning() {
return new FeedbackTask(
FeedbackType.CLEANING,
"""
You are a cleaning analyzer for a car rental company. Your job is to determine if a car needs cleaning based on feedback.
Analyze the feedback and car information to decide if a cleaning is needed.
If the feedback mentions dirt, mud, stains, or anything that suggests the car is dirty, recommend a cleaning.
Be specific about what type of cleaning is needed (exterior, interior, detailing, waxing).
If no interior or exterior car cleaning services are needed based on the feedback, respond with "CLEANING_NOT_REQUIRED".
Include the reason for your choice but keep your response short.
"""
);
}
/**
* Factory method for creating a maintenance feedback task.
*/
public static FeedbackTask maintenance() {
return new FeedbackTask(
FeedbackType.MAINTENANCE,
"""
You are a car maintenance analyzer for a car rental company. Your job is to determine if a car needs maintenance based on feedback.
Analyze the feedback and car information to decide if maintenance is needed.
If the feedback mentions mechanical issues, strange noises, performance problems, significant body damage or anything that suggests
the car needs maintenance, recommend appropriate maintenance.
Be specific about what type of maintenance is needed (oil change, tire rotation, brake service, engine service, transmission service, body work).
If no service of any kind, repairs or maintenance are needed, respond with "MAINTENANCE_NOT_REQUIRED".
Include the reason for your choice but keep your response short.
"""
);
}
/**
* Factory method for creating a disposition feedback task.
*/
public static FeedbackTask disposition() {
return new FeedbackTask(
FeedbackType.DISPOSITION,
"""
You are a disposition analyzer for a car rental company. Your job is to determine if a car should be considered for disposition (removal from fleet).
Analyze the feedback for SEVERE issues that would make the car uneconomical to keep:
- Major accidents: "wrecked", "totaled", "destroyed", "crashed", "collision"
- Severe damage: "frame damage", "structural damage", "major damage"
- Safety concerns: "unsafe", "not drivable", "inoperable", "dangerous"
- Catastrophic mechanical failure: "engine blown", "transmission failed", "major mechanical failure"
If you detect ANY of these severe issues, respond with:
"DISPOSITION_REQUIRED: [brief description of the severe issue]"
If the car has only minor or moderate issues that can be repaired, respond with:
"DISPOSITION_NOT_REQUIRED"
Keep your response concise.
"""
);
}
}
Each factory method returns a configured FeedbackTask with two important pieces of data:
- the feedback type (for identification and debugging)
- the system instructions to use for that analysis
This design keeps the agent generic and moves the task-specific behavior into data. If you ever want to add another feedback dimension later, you can usually start by adding another task factory rather than introducing another near-identical agent.
Create the Unified FeedbackAnalysisAgent
Now create the single feedback analyzer that can handle any of those tasks.
In src/main/java/com/carmanagement/agentic/agents, create FeedbackAnalysisAgent.java:
package com.carmanagement.agentic.agents;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.FeedbackTask;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
/**
* Unified agent that analyzes feedback based on the provided task configuration.
* This agent is parameterized to handle cleaning, maintenance, and disposition analysis.
*/
public interface FeedbackAnalysisAgent {
@SystemMessage("{task.systemInstructions}")
@UserMessage("""
Car Information:
Make: {carInfo.make}
Model: {carInfo.model}
Year: {carInfo.year}
Previous Condition: {carInfo.condition}
Feedback: {feedback}
""")
@Agent(description = "Feedback analyzer. Using feedback, determines if action is needed based on task type.",
outputKey = "feedbackAnalysis")
String analyzeFeedback(
FeedbackTask task,
CarInfo carInfo,
Integer carNumber,
String feedback);
}
The key detail here is the system message. Instead of hardcoding the instructions in the agent itself, the agent uses {task.systemInstructions}. That means the same agent can behave like a cleaning analyzer, a maintenance analyzer, or a disposition analyzer depending on the FeedbackTask that was passed in.
This is the core of the refactoring: one agent, multiple task configurations.
Create the FeedbackAnalysisResults Record
Once the parallel analysis is complete, we want to pass the results around as structured data rather than as a raw list of strings.
Create src/main/java/com/carmanagement/model/FeedbackAnalysisResults.java:
package com.carmanagement.model;
/**
* Record containing the three feedback analysis results.
* These are the outputs from the parallel feedback analysis workflow.
*/
public record FeedbackAnalysisResults(
String cleaningAnalysis,
String maintenanceAnalysis,
String dispositionAnalysis
) {
}
This record gives the rest of the workflow a stable, readable contract. Instead of dealing with array positions or ad hoc keys, later components can call cleaningAnalysis(), maintenanceAnalysis(), and dispositionAnalysis() directly.
Create the PricingAgent
The Miles & Smiles management has decided they feel comfortable using AI to determine the value of their cars to make further decisions on whether to keep or dispose the car. A wise decision? That remains to be seen 😉.
Either way, let’s create the agent. We’ll add some prompt engineering in the system message to guide the model on how to estimate value based on the brand, its state, and its age. The agent will be invoked by the supervisor when pricing is needed.
In src/main/java/com/carmanagement/agentic/agents, create PricingAgent.java:
package com.carmanagement.agentic.agents;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
/**
* Agent that estimates the market value of a vehicle.
* Used by the supervisor to make disposition decisions.
*/
public interface PricingAgent {
@SystemMessage("""
You are a vehicle pricing specialist with expertise in market valuations.
Today is {current_date}. Use this to calculate the current year and vehicle age.
Use these pricing guidelines:
Brand Base Values (new current-year models):
- Luxury brands (Mercedes-Benz, BMW, Audi): $50,000-$70,000
- Premium trucks (Ford F-150): $45,000-$60,000
- Mainstream brands (Toyota, Honda, Chevrolet): $28,000-$42,000
- Economy brands (Nissan): $22,000-$35,000
Depreciation (calculate age as: current year - vehicle year):
- Age 1 year (nearly new): -12% from base value
- Age 2 years: -15% additional (27% total depreciation)
- Age 3 years: -12% additional (39% total depreciation)
- Age 4 years: -10% additional (49% total depreciation)
- Age 5+ years: -8% per additional year
Condition Adjustments (apply after depreciation):
- Excellent/Like new: +5% to depreciated value
- Good/Recently serviced: No adjustment
- Fair/Minor issues: -10% from depreciated value
- Poor/Needs work: -20% from depreciated value
Provide:
1. Estimated market value (single dollar amount with comma separator)
2. Brief justification (2-3 sentences explaining age, condition, and brand factors)
Format your response as:
Estimated Value: $XX,XXX
Justification: [Your reasoning including vehicle age]
""")
@UserMessage("""
Estimate the current market value of this vehicle:
- Make: {carMake}
- Model: {carModel}
- Year: {carYear}
- Condition: {carCondition}
""")
@Agent(
outputKey = "carValue",
description = "Pricing specialist that estimates vehicle market value based on make, model, year, and condition"
)
String estimateValue(String carMake, String carModel, Integer carYear, String carCondition);
}
Create a DispositionAgent
Management also feels comfortable letting an AI model decide whether to SCRAP, SELL, DONATE, or KEEP the vehicle based on repair economics.
Create an agent that makes disposition decisions based on the pricing outcome as well as the car’s age and condition.
In src/main/java/com/carmanagement/agentic/agents, create DispositionAgent.java:
package com.carmanagement.agentic.agents;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
/**
* Agent that determines how to dispose of a car based on value, condition, and damage.
*/
public interface DispositionAgent {
@SystemMessage("""
You are a car disposition specialist for a car rental company.
Your job is to determine the best disposition action based on the car's value, condition, age, and damage.
Disposition Options:
- SCRAP: Car is beyond economical repair or has severe safety concerns
- SELL: Car has value but is aging out of the fleet or has moderate damage
- DONATE: Car has minimal value but could serve a charitable purpose
- KEEP: Car is worth keeping in the fleet
Decision Criteria:
- If estimated repair cost > 50% of car value: Consider SCRAP or SELL
- If car is over 5 years old with significant damage: SCRAP
- If car is 3-5 years old in fair condition: SELL
- If car has low value (<$5,000) but functional: DONATE
- If car is valuable and damage is minor: KEEP
Provide your recommendation with a clear explanation of the reasoning.
""")
@UserMessage("""
Determine the disposition for this vehicle:
- Make: {carMake}
- Model: {carModel}
- Year: {carYear}
- Car Number: {carNumber}
- Current Condition: {carCondition}
- Estimated Value: {carValue}
- Damage/Feedback: {feedback}
Provide your disposition recommendation (SCRAP/SELL/DONATE/KEEP) and explanation.
""")
@Agent(outputKey = "dispositionAction", description = "Car disposition specialist. Determines how to dispose of a car based on value and condition.")
String processDisposition(
String carMake,
String carModel,
Integer carYear,
Integer carNumber,
String carCondition,
String carValue,
String feedback);
}
Create the FleetSupervisorAgent
Now create the supervisor agent that orchestrates everything.
What matters most here is making the prompt as clear as possible about the workflow and the agents available to it. The more explicit you are, the better the supervisor can reason about which action agents to invoke.
In src/main/java/com/carmanagement/agentic/agents, create FleetSupervisorAgent.java:
package com.carmanagement.agentic.agents;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.FeedbackAnalysisResults;
import dev.langchain4j.agentic.declarative.SupervisorAgent;
import dev.langchain4j.agentic.declarative.SupervisorRequest;
/**
* Supervisor agent that orchestrates the entire car processing workflow.
* Coordinates action agents based on analysis results of the car condition.
*/
public interface FleetSupervisorAgent {
/**
* Main method to coordinate car processing based on feedback.
* This is the entry point for the supervisor agent.
*/
@SupervisorAgent(
outputKey = "supervisorDecision",
subAgents = {
PricingAgent.class,
DispositionAgent.class,
MaintenanceAgent.class,
CleaningAgent.class
}
)
String superviseCarProcessing(
CarInfo carInfo,
Integer carNumber,
FeedbackAnalysisResults feedbackAnalysisResults
);
/**
* Generates the supervisor request prompt based on feedback analysis results.
* This method examines the disposition analysis to determine if the car requires
* disposition (removal from fleet) and constructs appropriate instructions for
* the supervisor agent to coordinate the necessary action agents.
*
* @param carMake The make of the car
* @param carModel The model of the car
* @param carYear The year of the car
* @param carNumber The car's identification number
* @param carCondition The current condition description
* @param feedbackAnalysisResults The results from parallel feedback analysis
* @return A formatted prompt instructing the supervisor which agents to invoke
*/
@SupervisorRequest
static String request(
CarInfo carInfo,
Integer carNumber,
FeedbackAnalysisResults feedbackAnalysisResults
) {
boolean dispositionRequired = feedbackAnalysisResults.dispositionAnalysis() != null &&
feedbackAnalysisResults.dispositionAnalysis().toUpperCase().contains("DISPOSITION_REQUIRED");
String noDispositionMessage = """
No disposition has been requested.
INSTRUCTIONS:
- DO NOT invoke PricingAgent
- DO NOT invoke DispositionAgent
- Only invoke MaintenanceAgent if maintenance needed
- Only invoke CleaningAgent if cleaning needed
""";
// Disposition required - complex path
String dispositionMessage = """
The car has to be disposed.
STEP 1: Invoke PricingAgent to get car value
STEP 2: Invoke DispositionAgent to decide disposition action (SCRAP/SELL/DONATE/KEEP)
STEP 3: If DispositionAgent decides KEEP:
- Invoke MaintenanceAgent if maintenance needed
- Invoke CleaningAgent if cleaning needed
IMPORTANT: When invoking DispositionAgent:
- Pass carValue as a STRING with dollar sign (e.g., "$10,710" not 10710)
- Use the EXACT format from PricingAgent's response
Follow the decision logic in your system message carefully.
""";
return String.format("""
You are a fleet supervisor for a car rental company. You coordinate action agents based on feedback analysis.
The feedback has already been analyzed and you have these inputs:
- cleaningAnalysis: What cleaning is needed (or "CLEANING_NOT_REQUIRED")
- maintenanceAnalysis: What maintenance is needed (or "MAINTENANCE_NOT_REQUIRED")
- dispositionAnalysis: Whether severe damage requires disposition (or "DISPOSITION_NOT_REQUIRED")
Your job is to invoke the appropriate ACTION agents for this car
Car: %d %s %s (#%d)
Current Condition: %s
Cleaning Analysis: %s
Maintenance Analysis: %s
In particular, your have to follow these steps
%s
""",
carInfo.year, carInfo.make, carInfo.model, carNumber, carInfo.condition,
feedbackAnalysisResults.cleaningAnalysis(),
feedbackAnalysisResults.maintenanceAnalysis(),
dispositionRequired ? dispositionMessage : noDispositionMessage);
}
}
Key points:
- The
@SupervisorAgentannotation enables autonomous orchestration - The supervisor receives a
FeedbackAnalysisResultsobject and decides which action agents to invoke - Notice that the
subAgentslist contains only action agents. Feedback analysis has already been completed before the supervisor begins. - The prompt clearly explains both the available inputs and the routing expectations.
- The
@SupervisorRequestmethod provides the runtime request context for the supervisor.
Understanding @SupervisorRequest
The @SupervisorRequest annotation is what gives the supervisor its runtime instructions. This method inspects the feedback analysis results and builds a different request depending on whether disposition is required.
@SupervisorRequest
static String request(
String carMake,
String carModel,
Integer carYear,
Integer carNumber,
String carCondition,
FeedbackAnalysisResults feedbackAnalysisResults,
String rentalFeedback
) {
boolean dispositionRequired = feedbackAnalysisResults.dispositionAnalysis() != null &&
feedbackAnalysisResults.dispositionAnalysis().toUpperCase().contains("DISPOSITION_REQUIRED");
String noDispositionMessage = """
No disposition has been requested.
INSTRUCTIONS:
- DO NOT invoke PricingAgent
- DO NOT invoke DispositionAgent
- Only invoke MaintenanceAgent if maintenance needed
- Only invoke CleaningAgent if cleaning needed
""";
String dispositionMessage = """
The car has to be disposed.
STEP 1: Invoke PricingAgent to get car value
STEP 2: Invoke DispositionAgent to decide disposition action (SCRAP/SELL/DONATE/KEEP)
STEP 3: If DispositionAgent decides KEEP:
- Invoke MaintenanceAgent if maintenance needed
- Invoke CleaningAgent if cleaning needed
""";
// returns the final formatted request string...
}
This approach is useful because it keeps the supervisor’s instructions tightly aligned with the structured analysis results. The supervisor is not re-analyzing raw feedback from scratch. It is consuming the work already done by the feedback-analysis phase and using that to decide which actions to orchestrate.
Parallel Feedback Analysis with @ParallelMapperAgent
Step 03 introduced @ParallelAgent, which is ideal when you have a fixed set of different agents that should all run concurrently.
In this step, the situation is slightly different. We still want parallel execution, but we no longer have three different agent types. We have one reusable agent that should run once per task. That is exactly what @ParallelMapperAgent is for.
You can think of the difference like this:
@ParallelAgentsays: “run these different sub-agents in parallel”@ParallelMapperAgentsays: “take this list of items and run the same sub-agent in parallel for each item”
That second model is a much better fit when the only thing that varies is configuration.
Create the FeedbackAnalysisWorkflow
Now create the workflow that runs the unified agent once for each task.
Create src/main/java/com/carmanagement/agentic/workflow/FeedbackAnalysisWorkflow.java:
package com.carmanagement.agentic.workflow;
import com.carmanagement.agentic.agents.FeedbackAnalysisAgent;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.FeedbackAnalysisResults;
import com.carmanagement.model.FeedbackTask;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.ParallelMapperAgent;
import dev.langchain4j.agentic.scope.AgenticScope;
import java.util.List;
/**
* Workflow for processing car feedback in parallel.
* Analyzes feedback for cleaning, maintenance, and disposition needs using a unified agent.
*/
public interface FeedbackAnalysisWorkflow {
/**
* Runs the feedback analysis agent in parallel for multiple tasks.
* Uses @ParallelMapperAgent to execute the same agent with different task configurations.
* Returns a list of results that will be mapped to individual output keys.
*/
@ParallelMapperAgent(
description = "Analyzes car feedback in parallel for cleaning, maintenance, and disposition needs",
outputKey = "feedbackAnalysisResults",
subAgent = FeedbackAnalysisAgent.class,
itemsProvider = "tasks")
FeedbackAnalysisResults analyzeFeedback(
List<FeedbackTask> tasks,
CarInfo carInfo,
Integer carNumber,
String feedback);
/**
* Output method that transforms the parallel feedback results into a structured object.
* The feedbackAnalysisResults list contains results in the same order as the input tasks:
* [0] = cleaning analysis, [1] = maintenance analysis, [2] = disposition analysis
*/
@Output
static FeedbackAnalysisResults output(AgenticScope scope, List<String> feedbackAnalysisResults) {
return new FeedbackAnalysisResults(
feedbackAnalysisResults.get(0), // cleaningAnalysis
feedbackAnalysisResults.get(1), // maintenanceAnalysis
feedbackAnalysisResults.get(2) // dispositionAnalysis
);
}
}
A few things are happening here.
The @ParallelMapperAgent annotation points to FeedbackAnalysisAgent as the sub-agent and uses itemsProvider = "tasks" so the framework knows which collection to iterate over.
The method therefore receives a List<FeedbackTask> and executes the same analysis agent in parallel for each entry in that list.
The raw result of that execution is a List<String>, one result per task. That is useful, but it is not yet the shape we want for the rest of the workflow. The static @Output method solves that by converting the list into a FeedbackAnalysisResults record.
This is one of the nicest aspects of the pattern: the workflow can do its work in whatever low-level format is convenient, and then present a cleaner structured result to downstream components.
Why the @Output Method Matters
Without the @Output method, downstream agents would need to know that index 0 means cleaning, index 1 means maintenance, and index 2 means disposition. That would make the design more fragile and harder to understand.
By transforming the parallel results into FeedbackAnalysisResults, you create an explicit contract between workflow stages. The supervisor and final condition agent can then work with named properties instead of positional assumptions.
Update the CarProcessingWorkflow
Since we want the supervisor to determine which agents need to be called, we replace the previous conditional routing with the supervisor agent. The workflow now becomes a clean three-step sequence:
FeedbackAnalysisWorkflowperforms parallel analysisFleetSupervisorAgentdecides which action agents to invokeCarConditionFeedbackAgentsummarizes the outcome into structured car conditions
Update src/main/java/com/carmanagement/agentic/workflow/CarProcessingWorkflow.java:
package com.carmanagement.agentic.workflow;
import com.carmanagement.agentic.agents.CarConditionFeedbackAgent;
import com.carmanagement.agentic.agents.FleetSupervisorAgent;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.FeedbackTask;
import dev.langchain4j.agentic.declarative.Output;
import dev.langchain4j.agentic.declarative.SequenceAgent;
import io.quarkus.logging.Log;
import java.util.List;
/**
* Workflow for processing car returns using a supervisor agent for complete orchestration.
* The supervisor coordinates both feedback analysis and action agents.
*/
public interface CarProcessingWorkflow {
/**
* Processes a car return by first analyzing feedback, then using supervisor to coordinate actions.
* FeedbackAnalysisWorkflow analyzes feedback in parallel and returns FeedbackAnalysisResults via its @Output method.
* FleetSupervisorAgent uses these results to coordinate action agents.
* CarConditionFeedbackAgent determines the final car assignment and condition.
*/
@SequenceAgent(outputKey = "carProcessingAgentResult",
subAgents = { FeedbackAnalysisWorkflow.class, FleetSupervisorAgent.class, CarConditionFeedbackAgent.class })
CarConditions processCarReturn(
List<FeedbackTask> tasks,
CarInfo carInfo,
Integer carNumber,
String feedback);
@Output
static CarConditions output(CarConditions carConditions) {
// CarConditionFeedbackAgent handles all logic for determining
// the final car assignment and condition description.
Log.debug("CarConditions: " + carConditions.generalCondition() + " → " + carConditions.carAssignment());
return carConditions;
}
}
The important change is that the workflow now accepts a list of FeedbackTask values as input. That gives the feedback-analysis stage everything it needs to run the parameterized analysis.
Update the Final Condition Agent
Because the feedback-analysis phase now returns a structured results object, the final condition agent also becomes simpler and clearer.
Update src/main/java/com/carmanagement/agentic/agents/CarConditionFeedbackAgent.java:
package com.carmanagement.agentic.agents;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.FeedbackAnalysisResults;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
/**
* Agent that analyzes feedback to determine the final car condition and assignment.
* This is the final decision-maker that interprets all previous agent outputs.
*/
public interface CarConditionFeedbackAgent {
@SystemMessage("""
Analyze car processing results and output a JSON summary.
Output format:
{
"generalCondition": "concise description (max 200 chars)",
"carAssignment": "DISPOSITION|MAINTENANCE|CLEANING|NONE"
}
Rules:
- carAssignment: Check the ACTUAL DispositionAgent decision in supervisorDecision, not just the analysis
- If supervisorDecision mentions SCRAP/SELL/DONATE (but NOT KEEP) → DISPOSITION
- Else if maintenanceAnalysis ≠ "MAINTENANCE_NOT_REQUIRED" → MAINTENANCE
- Else if cleaningAnalysis ≠ "CLEANING_NOT_REQUIRED" → CLEANING
- Else → NONE
- IMPORTANT: If DispositionAgent decided KEEP, do NOT assign DISPOSITION - check maintenance/cleaning instead
- generalCondition: Summarize the action and reason
""")
@UserMessage("""
Car: {carInfo.year} {carInfo.make} {carInfo.model} (#{carNumber})
Supervisor Decision: {supervisorDecision}
Feedback Analysis Results:
- Disposition: {feedbackAnalysisResults.dispositionAnalysis}
- Maintenance: {feedbackAnalysisResults.maintenanceAnalysis}
- Cleaning: {feedbackAnalysisResults.cleaningAnalysis}
""")
@Agent(description = "Final car condition analyzer. Determines the car's condition and assignment based on all feedback.",
outputKey = "carConditions")
CarConditions analyzeForCondition(
CarInfo carInfo,
Integer carNumber,
FeedbackAnalysisResults feedbackAnalysisResults,
String supervisorDecision);
}
The agent now consumes FeedbackAnalysisResults directly. It also explicitly checks the actual supervisor decision so that a disposition analysis does not automatically imply a final disposition outcome. If the supervisor reaches a KEEP decision, the workflow can still fall back to maintenance or cleaning.
Update the Service Layer
Finally, the service layer needs to create the feedback tasks before invoking the workflow.
Update src/main/java/com/carmanagement/service/CarManagementService.java:
package com.carmanagement.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import com.carmanagement.agentic.workflow.CarProcessingWorkflow;
import com.carmanagement.model.CarConditions;
import com.carmanagement.model.CarInfo;
import com.carmanagement.model.CarStatus;
import com.carmanagement.model.FeedbackTask;
import io.quarkus.logging.Log;
import java.util.List;
/**
* Service for managing car returns from various operations.
*/
@ApplicationScoped
public class CarManagementService {
@Inject
CarProcessingWorkflow carProcessingWorkflow;
/**
* Process a car return from any operation.
*
* @param carNumber The car number
* @param feedback Optional feedback
* @return Result of the processing
*/
@Transactional
public String processCarReturn(Integer carNumber, String feedback) {
CarInfo carInfo = CarInfo.findById(carNumber);
if (carInfo == null) {
return "Car not found with number: " + carNumber;
}
// Create the list of feedback tasks
List<FeedbackTask> tasks = List.of(
FeedbackTask.cleaning(),
FeedbackTask.maintenance(),
FeedbackTask.disposition()
);
// Process the car return using the workflow with supervisor
CarConditions carConditions = carProcessingWorkflow.processCarReturn(
tasks,
carInfo,
carNumber,
feedback);
Log.info("CarConditionFeedbackAgent updating...");
// Update the car's condition with the result from CarConditionFeedbackAgent
carInfo.condition = carConditions.generalCondition();
// Update the car status based on the required action
switch (carConditions.carAssignment()) {
case DISPOSITION:
carInfo.status = CarStatus.PENDING_DISPOSITION;
Log.info("Car marked for disposition - awaiting final decision");
break;
case MAINTENANCE:
carInfo.status = CarStatus.IN_MAINTENANCE;
break;
case CLEANING:
carInfo.status = CarStatus.AT_CLEANING;
break;
case NONE:
carInfo.status = CarStatus.AVAILABLE;
break;
}
// Persist the changes to the database
carInfo.persist();
return carConditions.generalCondition();
}
}
This is where the list of tasks is assembled:
List<FeedbackTask> tasks = List.of(
FeedbackTask.cleaning(),
FeedbackTask.maintenance(),
FeedbackTask.disposition()
);
That list is then passed into the main workflow. This is a small change in code, but it is a significant architectural improvement. The service is now explicitly defining which analyses should run, and the workflow is generic enough to execute them without knowing about separate feedback agent types.
Why This Refactoring Is Worth It
At first glance, replacing three small agents with one configurable agent plus a task model might look like extra abstraction. In practice, it makes the design easier to maintain.
You now have:
- one place to evolve the shared feedback-analysis structure
- one workflow pattern that can scale to more analysis types
- a clean separation between what to analyze (
FeedbackTask) and how to analyze it (FeedbackAnalysisAgent) - a structured result object that is easier for downstream agents to consume
This refactoring is a good example of how to evolve a codebase: start with a simple, working solution, then gradually introduce abstractions that make the code more maintainable and scalable.
Try the Supervisor Pattern
You’ve now implemented the supervisor pattern together with a parameterized parallel analysis stage. Let’s test it.
Start the application:
Test Disposition Scenarios
Try these scenarios to see how the supervisor pattern autonomously orchestrates agents.
Scenario 1: Severe Damage - Disposition Required
Enter the following text in the feedback field for the Honda Civic:
What happens:
flowchart TD
Start([Input: Car was in serious collision<br/>Front end destroyed, airbags deployed])
Start --> FW[FeedbackAnalysisWorkflow<br/>Parallel Mapper]
FW --> T1[FeedbackTask.cleaning()]
FW --> T2[FeedbackTask.maintenance()]
FW --> T3[FeedbackTask.disposition()]
T1 --> A1[FeedbackAnalysisAgent]
T2 --> A1
T3 --> A1
A1 --> Results[FeedbackAnalysisResults]
Results --> FSA{FleetSupervisorAgent<br/>Autonomous Orchestration}
FSA -->|Disposition has highest priority| PA[Invoke PricingAgent]
PA --> PV[Estimate: $8,500<br/>2020 Honda Civic with severe damage]
PV --> DA[Invoke DispositionAgent]
DA --> DD[Decision: SCRAP<br/>Repair cost > 50% of value]
DD --> Result([Result: PENDING_DISPOSITION<br/>Condition: SCRAP - severe damage])
style FW fill:#FAE5D3
style FSA fill:#D5F5E3
style PA fill:#F9E79F
style DA fill:#F9E79F
style Result fill:#D2B4DE
Expected result:
- Status:
PENDING_DISPOSITION - Condition includes disposition decision, such as “SCRAP - severe damage, repair cost exceeds value”
PricingAgentestimated the car’s valueDispositionAgentmade a SCRAP decision based on economics
Scenario 2: Total Loss
Enter the following text in the Ford F-150 feedback field (status: In Maintenance) in the Fleet Status grid:
In this scenario, the car had already been sent to maintenance by the returns team, but the maintenance team is not able to repair it. The system can handle that scenario as well.
What happens:
flowchart TD
Start([Input: Car is totaled<br/>completely inoperable])
Start --> FW[FeedbackAnalysisWorkflow<br/>Parallel Mapper]
FW --> Results[FeedbackAnalysisResults]
Results --> FSA{FleetSupervisorAgent<br/>Autonomous Orchestration}
FSA -->|Severe damage detected| PA[Invoke PricingAgent]
PA --> PV[Estimate: $12,000<br/>2019 Toyota Camry, totaled]
PV --> DA[Invoke DispositionAgent]
DA --> DD[Decision: SCRAP or SELL<br/>Beyond economical repair]
DD --> Result([Result: PENDING_DISPOSITION<br/>Condition: SCRAP/SELL - totaled])
style FW fill:#FAE5D3
style FSA fill:#D5F5E3
style PA fill:#F9E79F
style DA fill:#F9E79F
style Result fill:#D2B4DE
Expected result:
- Status:
PENDING_DISPOSITION - Disposition decision: SCRAP or SELL
PricingAgentestimates value before the final disposition decisionDispositionAgentdetermines the vehicle is not worth repairing
Scenario 3: Repairable Damage
Enter the following text in the Mercedes Benz feedback field:
What happens:
flowchart TD
Start([Input: Engine making noise<br/>needs inspection])
Start --> FW[FeedbackAnalysisWorkflow<br/>Parallel Mapper]
FW --> Results[FeedbackAnalysisResults]
Results --> FSA{FleetSupervisorAgent<br/>Autonomous Orchestration}
FSA -->|No disposition, maintenance is priority| MA[Invoke MaintenanceAgent]
MA --> Result([Result: IN_MAINTENANCE<br/>Condition: Engine inspection required])
style FW fill:#FAE5D3
style FSA fill:#D5F5E3
style MA fill:#F9E79F
style Result fill:#D2B4DE
Expected result:
- Status:
IN_MAINTENANCE - Condition describes the maintenance issue
- Supervisor routes to
MaintenanceAgent, not disposition
Scenario 4: Minor Issues
Enter the following text in the BMW X5 feedback field (status: In Maintenance) in the Fleet Status grid:
What happens:
flowchart TD
Start([Input: Car is dirty<br/>needs cleaning])
Start --> FW[FeedbackAnalysisWorkflow<br/>Parallel Mapper]
FW --> Results[FeedbackAnalysisResults]
Results --> FSA{FleetSupervisorAgent<br/>Autonomous Orchestration}
FSA -->|No disposition or maintenance| CA[Invoke CleaningAgent]
CA --> Result([Result: IN_CLEANING<br/>Condition: Requires thorough cleaning])
style FW fill:#FAE5D3
style FSA fill:#D5F5E3
style CA fill:#F9E79F
style Result fill:#D2B4DE
Expected result:
- Status:
IN_CLEANING - Condition describes cleaning needs
- Supervisor routes only to
CleaningAgent
Experiment Further
1. Add More Feedback Tasks
Extend the FeedbackTask model with another factory method. For example, you could add a cosmetic-inspection task, a safety-compliance task, or an interior-damage task. Then update the list created in CarManagementService to include it.
This is a good way to see the benefit of the refactoring. You are extending behavior by adding configuration and workflow inputs rather than cloning another feedback agent.
2. Add More Disposition Criteria
Enhance the DispositionAgent to consider:
- repair history
- market demand for specific models
- seasonal factors
- fleet composition
3. Implement Multi-Tier Pricing
Create different pricing strategies:
- wholesale value for SCRAP decisions
- retail value for SELL decisions
- donation value for tax purposes
4. Add a Separate Disposition Workflow
Create a dedicated workflow for cars marked PENDING_DISPOSITION:
- get multiple price quotes
- check auction values
- evaluate donation options
- make a final disposition decision
Troubleshooting
Supervisor not invoking DispositionAgent
- Check that
FeedbackTask.disposition()includes the expected severe-damage instructions - Verify that
"DISPOSITION_REQUIRED"appears infeedbackAnalysisResults.dispositionAnalysis() - Review the
@SupervisorRequestlogic - Add logging to inspect the values inside
FeedbackAnalysisResults
Cars not getting PENDING_DISPOSITION status
- Check the output logic in
CarConditionFeedbackAgent - Verify that the supervisor decision actually contains a disposition outcome such as SCRAP, SELL, or DONATE
- Ensure
CarManagementServicemapsDISPOSITIONtoPENDING_DISPOSITION
Parallel analysis results look mismatched
- Verify that the task list in
CarManagementServiceis created in the same order expected by the@Outputmethod - Check that each
FeedbackTaskhas the correct instructions and output key
PricingAgent returning unexpected values
- Review the pricing guidelines in the
@SystemMessage - Check that car information is being passed correctly
- Verify the LLM is following the expected output format
What’s Next?
You’ve implemented the Supervisor Pattern for autonomous, context-aware orchestration and, along the way, refactored the feedback-analysis phase into a more maintainable parameterized design.
The system now:
- runs a single analysis agent multiple times in parallel with different
FeedbackTaskconfigurations - transforms those parallel results into a structured
FeedbackAnalysisResultsobject - lets a
FleetSupervisorAgentdecide which action agents to invoke - estimates vehicle value when disposition is needed
- makes economically informed SCRAP/SELL/DONATE/KEEP decisions
In Step 05, you’ll keep this refactored feedback-analysis architecture and add the Human-in-the-Loop (HITL) pattern so that high-value vehicle dispositions require human approval before execution.