Build and demo Java AI agent systems with langchain4j-agentic: workflow patterns, supervisor, custom Planner strategies (incl. the flagship typed-verdict / CriticResult-style critic pattern), plus MCP tools, A2A remote agents, build setup, and conference-demo storylines. Pinned to 1.15.0 / 1.15.0-beta25.
84
89%
Does it follow best practices?
Impact
100%
4.76xAverage score across 2 eval scenarios
Passed
No known issues
This is the headline pattern of the demo. It upgrades the generic "planner → executor → critic" loop by replacing the critic's stringly-typed score with a domain-modeled, strongly typed object that flows between agents and drives routing.
CriticResultis not a langchain4j-agentic type. It is a plain, user-defined illustration of the object that passes between agents in this workflow — the verifier's output that the router branches on. Name it and shape it to fit your domain (ItineraryReview,ComplianceVerdict, whatever). The generic<T>below is just to show the idea is reusable; making it generic is not a requirement. The point is "a typed domain object between agents," not this specific class.
Status: a framework-correct reference implementation of the demo's "money slide" concept — reconcile exact agent/method names with the live demo source. The
Planner/ActionAPI matches the official LangChain4j tutorial (verified).
The slide image is bundled with the skill at assets/agentic-useacase.jpg.
Start → [Identify Problem] → [Fix Problem] → [Verify Solution] → [Adjust] → … → Finish
│
CriticResult<T>
├─ successful == true → Finish, emit input (AccountIssueSolution)
└─ successful == false → Adjust, pass feedback (String) → re-verifyEach box can itself be a sub-workflow (nest freely — every builder returns an agent).
The whole point: the verifier does not emit a bare double or free text. It emits a typed object carrying the verdict, optional feedback, and the typed payload it judged — so the router can branch on real fields and the success path returns something usable.
CriticResult below is just an illustration of that object (the slide's name for it). It is ordinary application code, not a framework type — model it however fits your domain. A generic version makes it reusable across workflows, but a concrete per-domain type (e.g. ItineraryReview) is equally valid:
// Illustrative — your own type. Generic here only to show reusability.
public record CriticResult<T>(
boolean successful, // pass/fail verdict the router branches on
String feedback, // nullable — why it failed / how to adjust
T input // the typed artifact under review, e.g. the itinerary
) {
public static <T> CriticResult<T> pending() {
return new CriticResult<>(false, null, null);
}
}
// Equally fine — a concrete, non-generic shape for one domain:
// public record ItineraryReview(boolean approved, String notes, TravelItinerary itinerary) {}The slide's notation (successful: Boolean, feedback: String?, input: T) is Kotlin-style; use a Java record or a Kotlin data class — the framework parses either as structured output.
The domain artifact being verified is also a real type, not a string:
public record AccountIssueSolution(String accountId, String diagnosis, String resolution) {}Verify Solution returns the typed CriticResult<AccountIssueSolution>. LangChain4j parses the model output into the record (structured output), so downstream routing reads real fields.
public interface SolutionCritic {
@SystemMessage("You verify proposed account-issue solutions. Be strict.")
@UserMessage("""
Verify this proposed solution for the reported problem.
Problem: {{problem}}
Proposed solution: {{solution}}
Set successful=true only if it fully resolves the problem.
If not, set successful=false and give concrete feedback on what to adjust.
""")
@Agent(value = "Verifies a proposed solution and returns a typed verdict", outputKey = "criticResult")
CriticResult<AccountIssueSolution> verify(@V("problem") String problem,
@V("solution") AccountIssueSolution solution);
}
public interface Adjuster {
@UserMessage("""
The previous solution was rejected. Revise it using the feedback.
Problem: {{problem}}
Previous solution: {{solution}}
Reviewer feedback: {{feedback}}
""")
@Agent(value = "Adjusts a solution based on critic feedback", outputKey = "solution")
AccountIssueSolution adjust(@V("problem") String problem,
@V("solution") AccountIssueSolution solution,
@V("feedback") String feedback);
}
// plus IdentifyProblem (outputKey="problem") and FixProblem (outputKey="solution") agentsThe simplest framework primitive that produces the slide's behavior: loop (adjust, verify) and exit when the typed result says so.
UntypedAgent verifyAdjustLoop = AgenticServices.loopBuilder()
.subAgents(adjuster, critic) // adjust, then re-verify
.maxIterations(5)
.exitCondition(scope ->
scope.readState("criticResult", CriticResult.<AccountIssueSolution>pending()).successful())
.build();
UntypedAgent resolver = AgenticServices.sequenceBuilder()
.subAgents(identifyProblem, fixProblem, critic, verifyAdjustLoop)
.outputKey("criticResult")
// emit the TYPED solution on success — not a summary string
.output(scope ->
scope.readState("criticResult", CriticResult.<AccountIssueSolution>pending()).input())
.build();
AccountIssueSolution solution = (AccountIssueSolution) resolver.invoke(Map.of(
"problem", "",
"request", "Customer 8842 cannot log in after a password reset"));The Adjuster reads feedback — pull it from the critic result into the scope between iterations (a tiny listener or an output/transform step), or have the critic also publish feedback to its own key. The exit condition reads criticResult.successful(); the system output is criticResult.input(), the fully typed AccountIssueSolution.
Planner (this is the slide's title)"Custom Strategy" = a custom Planner that branches on the typed result. Use this when you want explicit control over the Identify→Fix→Verify→Adjust routing rather than a fixed loop.
The official Planner API (verified against the tutorial):
public interface Planner {
default void init(InitPlanningContext initPlanningContext) { }
default Action firstAction(PlanningContext planningContext) { return nextAction(planningContext); }
Action nextAction(PlanningContext planningContext); // returns call(...) or done()
}public class CriticDrivenStrategy implements Planner {
private AgentInstance identify, fix, verify, adjust;
private int iterations = 0;
private final int maxIterations;
public CriticDrivenStrategy(int maxIterations) { this.maxIterations = maxIterations; }
@Override public void init(InitPlanningContext ctx) {
List<AgentInstance> a = ctx.subagents(); // wire by name in real code
this.identify = a.get(0); this.fix = a.get(1);
this.verify = a.get(2); this.adjust = a.get(3);
}
@Override public Action firstAction(PlanningContext ctx) {
return call(identify); // Start → Identify Problem
}
@Override public Action nextAction(PlanningContext ctx) {
AgenticScope scope = ctx.agenticScope();
String last = ctx.previousAgentInvocation().agentName();
if (last.equals(identify.name())) return call(fix); // Identify → Fix
if (last.equals(fix.name())) return call(verify); // Fix → Verify
// After Verify (or after Adjust → re-verify), branch on the TYPED result
CriticResult<AccountIssueSolution> result =
scope.readState("criticResult", CriticResult.pending());
if (result.successful()) return done(); // successful==true → Finish
if (++iterations > maxIterations) return done(); // safety bound
if (last.equals(verify.name())) { // successful==false → Adjust
scope.writeState("feedback", result.feedback()); // pass feedback (String)
return call(adjust);
}
return call(verify); // Adjust → re-verify
}
}Build the system from the strategy, emitting the typed solution on success:
UntypedAgent resolver = AgenticServices.plannerBuilder()
.subAgents(identifyProblem, fixProblem, critic, adjuster)
.planner(() -> new CriticDrivenStrategy(5))
.outputKey("criticResult")
.output(scope ->
scope.readState("criticResult", CriticResult.<AccountIssueSolution>pending()).input())
.build();result.successful() and result.input() — real types — instead of Double.parseDouble(score) or scraping text. Refactors are compile-checked.AccountIssueSolution, ready to act on. The "input: T" edge in the slide is the typed artifact flowing straight to Finish.successful==false edge passes result.feedback() to Adjust, closing the refine loop deterministically.CriticResult isn't a framework type to adopt — it illustrates shaping the inter-agent payload to carry verdict + feedback + the typed artifact. Generic or concrete, both are fine; what matters is that it's typed, not a string or a score.AgentMonitor + HtmlReportGenerator for the topology/timeline reveal.SolutionCritic (judgment), smaller/faster for Adjuster — see tools-memory-and-build.md for model selection.AgentListener printing → identify / → fix / → verify (successful=…) / → adjust so the audience sees the branch decisions live (agent-configuration.md).HtmlReportGenerator.generateReport(monitor, Path.of("resolver-run.html")).