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
Features available on agents and workflows beyond basic composition.
By default, when an agent's required argument is missing from the AgenticScope, the whole system fails with MissingArgumentException. Mark an agent optional to silently skip it instead:
AudienceEditor audienceEditor = AgenticServices
.agentBuilder(AudienceEditor.class)
.chatModel(BASE_MODEL)
.optional(true)
.outputKey("story")
.build();
// No "audience" key provided -> audienceEditor is skipped, workflow continues
Map input = Map.of("topic", "dragons and wizards", "style", "fantasy");
String story = (String) novelCreator.invoke(input);Equivalent declaratively: @Agent(optional = true).
By default all invocations run on the calling thread (synchronous). Flag an agent async(true) to run it on a separate thread; the system proceeds without waiting. Its result lands in the AgenticScope when ready, and the scope only blocks on it when a later agent needs it as input.
FoodExpert foodExpert = AgenticServices
.agentBuilder(FoodExpert.class).chatModel(BASE_MODEL).async(true).outputKey("meals").build();
MovieExpert movieExpert = AgenticServices
.agentBuilder(MovieExpert.class).chatModel(BASE_MODEL).async(true).outputKey("movies").build();Flagging two independent agents as async makes them effectively run in parallel even inside a sequence workflow.
An agent can return a TokenStream and be configured with a streamingChatModel:
public interface StreamingCreativeWriter {
@UserMessage("""
You are a creative writer.
Generate a draft of a story no more than 3 sentences long around the given topic.
Return only the story and nothing else.
The topic is {{topic}}.
""")
@Agent("Generates a story based on the given topic")
TokenStream generateStory(@V("topic") String topic);
}
StreamingCreativeWriter creativeWriter = AgenticServices.agentBuilder(StreamingCreativeWriter.class)
.streamingChatModel(streamingBaseModel())
.outputKey("story")
.build();
TokenStream tokenStream = creativeWriter.generateStory("dragons and wizards");Key rule: inside a system, a streaming agent only propagates its stream to the caller when it is the last agent invoked. Otherwise it behaves like an async agent — earlier streaming agents are fully consumed before the next agent starts, and only the final agent's stream is surfaced.
chatModel (and streamingChatModel) have an overload taking a Function<AgenticScope, ChatModel>, evaluated before every invocation — pick a cheaper or stronger model based on current state:
StoryEditor storyEditor = AgenticServices.agentBuilder(StoryEditor.class)
.chatModel(scope -> {
CritiqueResult critique = (CritiqueResult) scope.readState("critique");
return critique != null && critique.score() > 7.8 ? enhancedModel() : baseModel();
})
.outputKey("story")
.build();Provide an errorHandler that maps an ErrorContext to an ErrorRecoveryResult:
record ErrorContext(String agentName, AgenticScope agenticScope, AgentInvocationException exception) { }Three recovery outcomes:
ErrorRecoveryResult.throwException() — default; propagate the error to the caller.ErrorRecoveryResult.retry() — retry the invocation, optionally after corrective action.ErrorRecoveryResult.result(Object result) — ignore the failure and substitute a result.UntypedAgent novelCreator = AgenticServices.sequenceBuilder()
.subAgents(creativeWriter, audienceEditor, styleEditor)
.errorHandler(errorContext -> {
if (errorContext.agentName().equals("generateStory")
&& errorContext.exception() instanceof MissingArgumentException mEx
&& mEx.argumentName().equals("topic")) {
errorContext.agenticScope().writeState("topic", "dragons and wizards");
return ErrorRecoveryResult.retry();
}
return ErrorRecoveryResult.throwException();
})
.outputKey("story")
.build();Register an AgentListener via .listener(...) on any builder:
public interface AgentListener {
default void beforeAgentInvocation(AgentRequest agentRequest) { }
default void afterAgentInvocation(AgentResponse agentResponse) { }
default void onAgentInvocationError(AgentInvocationError agentInvocationError) { }
default void afterAgenticScopeCreated(AgenticScope agenticScope) { }
default void beforeAgenticScopeDestroyed(AgenticScope agenticScope) { }
default void beforeToolExecution(BeforeToolExecution beforeToolExecution) { }
default void afterToolExecution(ToolExecution toolExecution) { }
default boolean inheritedBySubagents() { return false; }
}All methods have empty defaults. AgentRequest/AgentResponse expose the agent name, inputs, output, and the AgenticScope. Listeners run on the invocation thread — do not block.
Listeners are:
.listener(...) multiple times; invoked in registration order.true from inheritedBySubagents() so a listener on a top-level agent is also notified for all sub-agents at any depth.AgentMonitorAgentMonitor is a built-in AgentListener (inherited by sub-agents) that records every invocation into an in-memory tree (start/finish time, duration, tokens, inputs, output).
AgentMonitor monitor = new AgentMonitor();
UntypedAgent styledWriter = AgenticServices.sequenceBuilder()
.subAgents(creativeWriter, styleReviewLoop)
.listener(monitor)
.listener(new AgentListener() {
@Override public void afterAgentInvocation(AgentResponse response) {
if (response.agentName().equals("styleScorer")) {
System.out.println("Current score: " + response.output());
}
}
})
.outputKey("story")
.build();
String story = styledWriter.invoke(Map.of("topic", "dragons and wizards", "style", "comedy"));
MonitoredExecution execution = monitor.successfulExecutions().get(0);
System.out.println(execution); // prints the nested invocation treeHtmlReportGenerator.generateReport(monitor, Path.of("review-loop.html")); // topology + executions
HtmlReportGenerator.generateTopology(styledWriter, Path.of("topology.html")); // topology only
HtmlReportGenerator.generateExecution(monitor, Path.of("execution.html")); // executions only
// generateExecution also supports filtering by memoryId; all methods have String-returning overloadsMonitoredAgent shortcutMake the agent interface extend MonitoredAgent and a monitor is created and registered automatically:
public interface StyledWriter extends MonitoredAgent {
@Agent("Write a creative story about the given topic")
String generateStoryWithStyle(@V("topic") String topic, @V("style") String style);
}
StyledWriter styledWriter = AgenticServices.sequenceBuilder(StyledWriter.class)
.subAgents(creativeWriter, styleReviewLoop)
.outputKey("story")
.build();
AgentMonitor monitor = styledWriter.agentMonitor();