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

agent-configuration.mddocs/

Agent Configuration & Cross-Cutting Features

Features available on agents and workflows beyond basic composition.


Optional agents

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


Asynchronous agents

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.


Streaming agents

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.


Dynamic chat model selection

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

Error handling

Provide an errorHandler that maps an ErrorContext to an ErrorRecoveryResult:

record ErrorContext(String agentName, AgenticScope agenticScope, AgentInvocationException exception) { }

Three recovery outcomes:

  1. ErrorRecoveryResult.throwException() — default; propagate the error to the caller.
  2. ErrorRecoveryResult.retry() — retry the invocation, optionally after corrective action.
  3. 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();

Observability

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:

  • composable — call .listener(...) multiple times; invoked in registration order.
  • optionally hierarchical — return true from inheritedBySubagents() so a listener on a top-level agent is also notified for all sub-agents at any depth.

Monitoring with AgentMonitor

AgentMonitor 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 tree

HTML reports

HtmlReportGenerator.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 overloads

MonitoredAgent shortcut

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

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