Quarkus extension that integrates LangChain4j's agentic capabilities, enabling developers to build AI agent-based applications using declarative patterns with support for multiple agent types, agent-to-agent communication, and CDI integration.
This document describes how to access and manipulate shared state across agents in a workflow using AgenticScope.
Provides access to shared state across agents in a workflow.
/**
* Interface for accessing shared state across agents
*/
public interface AgenticScope {
/**
* Reads state value by key
* @param key - State key
* @return State value, or null if not found
*/
<T> T readState(String key);
/**
* Reads state value by key with default
* @param key - State key
* @param defaultValue - Default value if key not found
* @return State value, or defaultValue if not found
*/
<T> T readState(String key, T defaultValue);
/**
* Writes state value
* @param key - State key
* @param value - State value to write
*/
<T> void writeState(String key, T value);
}Usage Example:
public interface DataProcessingAgent {
@SequenceAgent(
outputKey = "finalResult",
subAgents = { Validator.class, Transformer.class, Enricher.class }
)
ResultWithAgenticScope<String> processData(@V("rawData") String rawData);
}
public interface Validator {
@Agent(description = "Validates data", outputKey = "validatedData")
String validate(@V("rawData") String rawData);
// Validation result stored in scope with outputKey "validatedData"
}
public interface Transformer {
@Agent(description = "Transforms data", outputKey = "transformedData")
String transform(@V("validatedData") String validatedData);
// Can access validatedData from scope via @V parameter
// Result stored with outputKey "transformedData"
}
// Usage
@Inject
DataProcessingAgent processor;
ResultWithAgenticScope<String> result = processor.processData("raw input");
// Access scope to read intermediate states
AgenticScope scope = result.agenticScope();
String validated = scope.readState("validatedData");
String transformed = scope.readState("transformedData");
String finalResult = result.result();Wrapper that returns both the agent result and access to the agentic scope.
/**
* Wrapper containing both result and agentic scope
*
* @param <T> - Type of the result
*/
public class ResultWithAgenticScope<T> {
/**
* Gets the actual result
* @return The result value
*/
T result();
/**
* Gets the agentic scope
* @return The AgenticScope instance
*/
AgenticScope agenticScope();
}Usage Example:
public interface AnalysisAgent {
@SequenceAgent(
outputKey = "analysis",
subAgents = { DataGatherer.class, Analyzer.class, Reporter.class }
)
ResultWithAgenticScope<String> analyzeData(@V("query") String query);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
AnalysisAgent agent;
ResultWithAgenticScope<String> result = agent.analyzeData("Q4 sales trends");
// Access the final result
String analysis = result.result();
// Access intermediate data from scope
AgenticScope scope = result.agenticScope();
String gatheredData = scope.readState("data");
String rawAnalysis = scope.readState("rawAnalysis");
// Check metadata
String query = scope.readState("query");Marker interface that provides direct access to AgenticScope within agent interfaces.
/**
* Marker interface that provides access to agentic scope
*/
public interface AgenticScopeAccess {
/**
* Gets the current agentic scope
* @return AgenticScope instance
*/
AgenticScope agenticScope();
}Usage Example:
public interface StatefulAgent extends AgenticScopeAccess {
@Agent(description = "Processes with state access", outputKey = "result")
String process(@V("input") String input);
// Can define helper methods that access scope
default void logState() {
AgenticScope scope = agenticScope();
System.out.println("Current state keys: " + scope.readState("keys"));
}
default String getPreviousResult() {
return agenticScope().readState("previousResult", "none");
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
StatefulAgent agent;
String result = agent.process("input data");
agent.logState();
String previous = agent.getPreviousResult();Marker interface that provides access to chat message history within agent interfaces.
/**
* Marker interface that provides access to chat message history
* Allows agents to inspect the conversation history
*/
public interface ChatMessagesAccess {
/**
* Gets the list of chat messages exchanged so far
* @return List of ChatMessage objects
*/
List<ChatMessage> chatMessages();
}Usage Example:
import dev.langchain4j.agentic.agent.ChatMessagesAccess;
import dev.langchain4j.data.message.ChatMessage;
public interface ConversationAnalyzer extends ChatMessagesAccess {
@Agent(description = "Analyzes conversation patterns", outputKey = "analysis")
String analyzeConversation(@V("query") String query);
// Can access chat history
default int getMessageCount() {
return chatMessages().size();
}
default String getLastUserMessage() {
List<ChatMessage> messages = chatMessages();
for (int i = messages.size() - 1; i >= 0; i--) {
ChatMessage msg = messages.get(i);
if (msg instanceof dev.langchain4j.data.message.UserMessage userMsg) {
// UserMessage contains contents, extract text from first content
if (!userMsg.contents().isEmpty()) {
var content = userMsg.contents().get(0);
if (content instanceof dev.langchain4j.data.message.TextContent textContent) {
return textContent.text();
}
}
}
}
return null;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage
@Inject
ConversationAnalyzer analyzer;
String analysis = analyzer.analyzeConversation("What patterns do you see?");
int messageCount = analyzer.getMessageCount();public interface Step1Agent {
@Agent(description = "First processing step", outputKey = "step1Result")
String step1(@V("input") String input);
}
public interface Step2Agent {
@Agent(description = "Second processing step", outputKey = "step2Result")
String step2(@V("step1Result") String previousResult);
// Reads step1Result from scope automatically via @V
}
public interface Step3Agent {
@Agent(description = "Final processing step", outputKey = "finalResult")
String step3(@V("step1Result") String first, @V("step2Result") String second);
// Can access multiple previous outputs
}
public interface Pipeline {
@SequenceAgent(
outputKey = "finalResult",
subAgents = { Step1Agent.class, Step2Agent.class, Step3Agent.class }
)
ResultWithAgenticScope<String> execute(@V("input") String input);
}public interface StatefulWorkflow {
@Agent(description = "Processes with state tracking", outputKey = "result")
String process(@V("data") String data);
@ErrorHandler
static ErrorRecoveryResult handleError(ErrorContext context) {
AgenticScope scope = context.agenticScope();
// Read current state
Integer retryCount = scope.readState("retryCount", 0);
String lastGoodState = scope.readState("lastGoodState");
// Update state
scope.writeState("retryCount", retryCount + 1);
scope.writeState("lastError", context.exception().getMessage());
// Decide recovery strategy based on state
if (retryCount < 3) {
// Restore last good state if available
if (lastGoodState != null) {
scope.writeState("data", lastGoodState);
}
return ErrorRecoveryResult.retry();
}
return ErrorRecoveryResult.throwException();
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}public interface ConditionalWorkflow {
@ConditionalAgent(
outputKey = "result",
subAgents = { OptionA.class, OptionB.class, OptionC.class }
)
String route(@V("input") String input);
@ActivationCondition(OptionA.class)
static boolean activateA(@V("score") double score, @V("category") String category) {
// Decisions based on multiple state values
return score > 0.8 && "premium".equals(category);
}
@ActivationCondition(OptionB.class)
static boolean activateB(@V("score") double score) {
return score > 0.5 && score <= 0.8;
}
@ActivationCondition(OptionC.class)
static boolean activateC(@V("score") double score) {
return score <= 0.5;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}public interface IterativeProcessor {
@LoopAgent(
description = "Processes iteratively",
outputKey = "refinedResult",
maxIterations = 10,
subAgents = { Evaluator.class, Refiner.class }
)
ResultWithAgenticScope<String> process(@V("input") String input);
@ExitCondition
static boolean shouldStop(@V("score") double score, @V("improvementRate") double improvement) {
// Exit based on multiple criteria
return score >= 0.9 || improvement < 0.01;
}
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
public interface Evaluator {
@Agent(description = "Evaluates result", outputKey = "score")
double evaluate(@V("refinedResult") String result);
}
public interface Refiner {
@Agent(description = "Refines result", outputKey = "refinedResult")
String refine(@V("refinedResult") String current, @V("score") double score);
// Custom logic could track improvement rate
default void trackImprovement(AgenticScope scope, double newScore) {
Double previousScore = scope.readState("previousScore", 0.0);
double improvement = newScore - previousScore;
scope.writeState("improvementRate", improvement);
scope.writeState("previousScore", newScore);
}
}@ApplicationScoped
public class WorkflowService {
@Inject
ComplexWorkflowAgent workflow;
public WorkflowReport executeAndReport(String input) {
ResultWithAgenticScope<String> result = workflow.execute(input);
// Build comprehensive report from scope
AgenticScope scope = result.agenticScope();
return new WorkflowReport(
result.result(),
scope.readState("step1Output"),
scope.readState("step2Output"),
scope.readState("validationScore", 0.0),
scope.readState("processingTime", 0L),
scope.readState("warnings", List.of())
);
}
}The AgenticScope.readState() method returns Object and requires explicit type handling by developers.
public interface TypeSafeWorkflow {
@Agent(description = "Processes with typed state", outputKey = "result")
String process(@V("input") String input);
@ChatModelSupplier
static ChatModel chatModel() {
return new OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.build();
}
}
// Usage with type safety
@Inject
TypeSafeWorkflow workflow;
ResultWithAgenticScope<String> result = workflow.process("data");
AgenticScope scope = result.agenticScope();
// Type-safe reads with defaults (recommended approach)
// The default value provides type inference
Integer count = scope.readState("count", 0);
Double score = scope.readState("score", 0.0);
Boolean validated = scope.readState("validated", false);
List<String> tags = scope.readState("tags", List.of());
Map<String, Object> metadata = scope.readState("metadata", Map.of());
// Alternative: Explicit casting when not using defaults
String name = (String) scope.readState("name");
Integer age = (Integer) scope.readState("age");
// Alternative: Using type parameter (if available in your version)
String value = scope.<String>readState("key");
// Important: readState() returns Object
// Always provide a default value OR cast the result explicitly
// Without proper type handling, you'll get compilation errorsType Safety Guidelines:
readState(key, defaultValue) overload - the default value provides type safety(String) scope.readState(key)readState(key) without casting will cause type mismatch errorsObject, so Java cannot infer the type automaticallyThe agentic scope lifecycle in multi-agent workflows:
Example:
public interface LifecycleDemo {
// Step 1: Scope created, "input" written
@SequenceAgent(
outputKey = "final",
subAgents = { AgentA.class, AgentB.class, AgentC.class }
)
ResultWithAgenticScope<String> demo(@V("input") String input);
}
public interface AgentA {
// Step 2: Reads "input", writes "outputA"
@Agent(outputKey = "outputA")
String processA(@V("input") String input);
}
public interface AgentB {
// Step 3: Reads "outputA", writes "outputB"
@Agent(outputKey = "outputB")
String processB(@V("outputA") String previous);
}
public interface AgentC {
// Step 4: Reads "outputA" and "outputB", writes "final"
@Agent(outputKey = "final")
String processC(@V("outputA") String a, @V("outputB") String b);
}
// Step 5: After completion, full scope available
ResultWithAgenticScope<String> result = agent.demo("test");
AgenticScope scope = result.agenticScope(); // Access full historyInstall with Tessl CLI
npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-agentic