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
When the sequence of agent invocations can't be fixed in advance, control flow is decided at runtime — either by an LLM (supervisor) or by an algorithm (custom Planner).
A supervisor agent is given sub-agents and autonomously generates a plan, deciding which to invoke next or whether the task is done.
Example: banking agents using tools.
public interface WithdrawAgent {
@SystemMessage("You are a banker that can only withdraw US dollars (USD) from a user account.")
@UserMessage("Withdraw {{amount}} USD from {{user}}'s account and return the new balance.")
@Agent("A banker that withdraw USD from an account")
String withdraw(@V("user") String user, @V("amount") Double amount);
}
// CreditAgent and ExchangeAgent defined similarly; ExchangeAgent uses an ExchangeToolTools are plain @Tool-annotated classes (e.g. a BankTool keeping balances, an ExchangeTool hitting a FX REST API). Build the agents with .tools(...), then a supervisor:
BankTool bankTool = new BankTool();
bankTool.createAccount("Mario", 1000.0);
bankTool.createAccount("Georgios", 1000.0);
WithdrawAgent withdrawAgent = AgenticServices.agentBuilder(WithdrawAgent.class)
.chatModel(BASE_MODEL).tools(bankTool).build();
CreditAgent creditAgent = AgenticServices.agentBuilder(CreditAgent.class)
.chatModel(BASE_MODEL).tools(bankTool).build();
ExchangeAgent exchangeAgent = AgenticServices.agentBuilder(ExchangeAgent.class)
.chatModel(BASE_MODEL).tools(new ExchangeTool()).build();
SupervisorAgent bankSupervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.subAgents(withdrawAgent, creditAgent, exchangeAgent)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();Sub-agents may themselves be complex workflows — the supervisor sees each as a single agent. Typical signature:
public interface SupervisorAgent {
@Agent
String invoke(@V("request") String request);
}bankSupervisor.invoke("Transfer 100 EUR from Mario's account to Georgios' one");Internally the planner produces a sequence of AgentInvocation(String agentName, Map arguments), ending with a special done invocation carrying the summary:
AgentInvocation{agentName='exchange', arguments={originalCurrency=EUR, amount=100, targetCurrency=USD}}
AgentInvocation{agentName='withdraw', arguments={user=Mario, amount=115.0}}
AgentInvocation{agentName='credit', arguments={user=Georgios, amount=115.0}}
AgentInvocation{agentName='done', arguments={response=The transfer ... completed ...}}What the supervisor returns:
public enum SupervisorResponseStrategy { SCORED, SUMMARY, LAST }LAST (default) — the output of the last invoked agent. Best when the user wants the artifact (e.g. the final story), not a recap.SUMMARY — the planner's summary of all steps (best for multi-step transactions like the bank transfer).SCORED — a second scorer agent receives the original request plus both candidate responses (final vs summary) and picks the better fit, e.g. ResponseScore{finalResponse=0.3, summary=1.0}.AgenticServices.supervisorBuilder()
.responseStrategy(SupervisorResponseStrategy.SCORED)
.build();How the supervisor builds the context it reasons over:
public enum SupervisorContextStrategy { CHAT_MEMORY, SUMMARIZATION, CHAT_MEMORY_AND_SUMMARIZATION }AgenticServices.supervisorBuilder()
.contextGenerationStrategy(SupervisorContextStrategy.SUMMARIZATION)
.build();Default is local chat memory; summarization condenses sub-agent conversations.
Stored in AgenticScope under supervisorContext. Three ways to set it:
// 1. Build-time
AgenticServices.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.supervisorContext("Policies: prefer internal tools; currency USD; no external APIs")
.subAgents(withdrawAgent, creditAgent, exchangeAgent)
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();
// 2. Typed invocation parameter
public interface SupervisorAgent {
@Agent
String invoke(@V("request") String request, @V("supervisorContext") String supervisorContext);
}
// 3. Untyped: put "supervisorContext" in the input map
Map input = Map.of(
"request", "Transfer 100 EUR from Mario's account to Georgios' one",
"supervisorContext", "Policies: convert to USD first; use bank tools only; no external APIs");If both build-time and invocation values are present, the invocation value wins.
Planner abstractionWhen no built-in pattern fits, implement a Planner — a spec of the execution plan over sub-agents. All built-in patterns are themselves Planner implementations.
public interface Planner {
default void init(InitPlanningContext initPlanningContext) { }
default Action firstAction(PlanningContext planningContext) { return nextAction(planningContext); }
Action nextAction(PlanningContext planningContext);
}init — once at start; initialize state (e.g. capture sub-agents).firstAction — the first step (defaults to nextAction).nextAction — called after each agent execution to decide the next step.Action is either a call to one or more sub-agents or done(). A single agent runs sequentially on the planner's thread; multiple agents run in parallel via the provided Executor. Helpers: call(...), done().
Built-in examples:
public class ParallelPlanner implements Planner {
private List<AgentInstance> agents;
@Override public void init(InitPlanningContext ctx) { this.agents = ctx.subagents(); }
@Override public Action firstAction(PlanningContext ctx) { return call(agents); }
@Override public Action nextAction(PlanningContext ctx) { return done(); }
}
public class SequentialPlanner implements Planner {
private List<AgentInstance> agents;
private int agentCursor = 0;
@Override public void init(InitPlanningContext ctx) { this.agents = ctx.subagents(); }
@Override public Action nextAction(PlanningContext ctx) {
return agentCursor >= agents.size() ? done() : call(agents.get(agentCursor++));
}
}Build a system from a planner with plannerBuilder() + a Supplier<Planner>:
UntypedAgent novelCreator = AgenticServices.plannerBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.outputKey("story")
.planner(SequentialPlanner::new)
.build();(equivalent to sequenceBuilder().) Planner-based systems compose with any built-in pattern.
The following three patterns ship in the langchain4j-agentic-patterns module.
A middle ground between rigid workflows and the fully-flexible supervisor: agents work toward a goal, but the invocation order is computed algorithmically. Each agent's required inputs/produced outputs act as pre/postconditions; the system's goal is its desired output. A dependency graph is built and the shortest path from the initial state to the goal is searched.
public class GoalOrientedPlanner implements Planner {
private String goal;
private GoalOrientedSearchGraph graph;
private List<AgentInstance> path;
private int agentCursor = 0;
@Override public void init(InitPlanningContext ctx) {
this.goal = ctx.plannerAgent().outputKey();
this.graph = new GoalOrientedSearchGraph(ctx.subagents());
}
@Override public Action firstAction(PlanningContext ctx) {
path = graph.search(ctx.agenticScope().state().keySet(), goal);
if (path.isEmpty()) throw new IllegalStateException("No path found for goal: " + goal);
return call(path.get(agentCursor++));
}
@Override public Action nextAction(PlanningContext ctx) {
return agentCursor >= path.size() ? done() : call(path.get(agentCursor++));
}
}Example — extract person & sign, generate horoscope, find a story (web search tool), write a writeup:
UntypedAgent horoscopeAgent = AgenticServices.plannerBuilder()
.subAgents(horoscopeGenerator, personExtractor, signExtractor, writer, storyFinder)
.outputKey("writeup")
.planner(GoalOrientedPlanner::new)
.build();
String writeup = horoscopeAgent.invoke(Map.of("prompt", "My name is Mario and my zodiac sign is pisces"));
// Computed path: [extractPerson, extractSign, horoscope, findStory, write]Limitation: the shortest-path approach structurally cannot express loops — nest a loop workflow as a sub-agent when iteration is needed.
Decentralized: no top-level coordinator. All agents are equal peers coordinated through AgenticScope state. An agent is triggered when its required inputs are present; a change to those variables (from another agent's output) can re-trigger it. Terminates when the scope is stable (no agent can run), the exit condition holds, or max invocations is reached.
public class P2PPlanner implements Planner {
private final int maxAgentsInvocations;
private final BiPredicate<AgenticScope, Integer> exitCondition;
private int invocationCounter = 0;
private Map<String, AgentActivator> agentActivators;
public P2PPlanner(int maxAgentsInvocations, BiPredicate<AgenticScope, Integer> exitCondition) {
this.maxAgentsInvocations = maxAgentsInvocations;
this.exitCondition = exitCondition;
}
@Override public void init(InitPlanningContext ctx) {
this.agentActivators = ctx.subagents().stream()
.collect(toMap(AgentInstance::agentId, AgentActivator::new));
}
@Override public Action nextAction(PlanningContext ctx) {
if (terminated(ctx.agenticScope())) return done();
AgentActivator last = agentActivators.get(ctx.previousAgentInvocation().agentId());
last.finishExecution();
agentActivators.values().forEach(a -> a.onStateChanged(last.agent.outputKey()));
return nextCallAction(ctx.agenticScope());
}
private Action nextCallAction(AgenticScope scope) {
AgentInstance[] toCall = agentActivators.values().stream()
.filter(a -> a.canActivate(scope))
.peek(AgentActivator::startExecution)
.map(AgentActivator::agent)
.toArray(AgentInstance[]::new);
invocationCounter += toCall.length;
return call(toCall);
}
private boolean terminated(AgenticScope scope) {
return invocationCounter > maxAgentsInvocations || exitCondition.test(scope, invocationCounter);
}
}Example — a scientific research system (literature → hypothesis → critique → validation, with a scorer) over agents that share an ArxivCrawler tool:
ResearchAgent researcher = AgenticServices.plannerBuilder(ResearchAgent.class)
.subAgents(literatureAgent, hypothesisAgent, criticAgent, validationAgent, scorerAgent)
.outputKey("hypothesis")
.planner(() -> new P2PPlanner(10, (agenticScope, count) -> {
if (!agenticScope.hasState("score")) return false;
return agenticScope.readState("score", 0.0) >= 0.85;
}))
.build();
String hypothesis = researcher.research("black holes");Flow: only literatureAgent can start (only it has topic); its researchFindings triggers hypothesisAgent; the hypothesis triggers criticAgent; validationAgent consumes hypothesis + critique to emit a new hypothesis (re-triggering peers); scorerAgent scores it. Stops at score ≥ 0.85 or 10 invocations.
Run multiple agents on the same input in parallel, collect their outputs as votes, and reconcile them via a pluggable VotingStrategy. Useful for classification, content moderation, and risk assessment, where consensus across diverse prompts/models beats a single agent's judgment.
public class VotingPlanner implements Planner {
private final VotingStrategy strategy;
private List<AgentInstance> subagents;
// dispatches all sub-agents in parallel, waits for all, then aggregates outputs via the VotingStrategy
}Build it with plannerBuilder() and a VotingPlanner supplier configured with your VotingStrategy, passing the agents that should each independently evaluate the same input.
Verify the exact
VotingPlanner/VotingStrategyconstructor signatures against the currentlangchain4j-agentic-patternssource, as this pattern is the newest.