CtrlK
BlogDocsLog inGet started
Tessl Logo

gamussa/langchain4j-agentic

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

4.76x
Quality

89%

Does it follow best practices?

Impact

100%

4.76x

Average score across 2 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

pure-agentic.mddocs/

Pure Agentic AI: Supervisor & Custom Planners

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).


Supervisor pattern

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 ExchangeTool

Tools 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 ...}}

Response strategy

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();

Context strategy

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.

Providing supervisor context (policies/constraints)

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.


Custom patterns: the Planner abstraction

When 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.


Goal-oriented (GOAP) pattern

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.


Peer-to-peer (P2P) pattern

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.


Voting (council) pattern

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/VotingStrategy constructor signatures against the current langchain4j-agentic-patterns source, as this pattern is the newest.

docs

agent-configuration.md

agents-and-scope.md

custom-strategy-critic-result.md

declarative-api.md

demo-storylines.md

gotchas.md

index.md

mcp-and-a2a.md

pure-agentic.md

tools-memory-and-build.md

workflow-patterns.md

README.md

tile.json