LangChain4j Agentic Framework provides a comprehensive Java library for building multi-agent AI systems with support for workflow orchestration, supervisor agents, planning-based execution, declarative configuration, agent-to-agent communication, and human-in-the-loop workflows.
Complete API reference for AgenticScope - shared state environment for agents within an agentic system.
import dev.langchain4j.agentic.scope.AgenticScope;
@Agent
String myAgent(AgenticScope scope, String input) {
// Write state
scope.writeState("result", processedData);
// Read state
String previousData = (String) scope.readState("data");
// Check state exists
if (scope.hasState("config")) {
// Use config
}
return "Complete";
}Get the memory ID associated with the agentic scope.
/**
* Get memory ID associated with this scope
* @return Memory ID object
*/
Object memoryId();Usage Examples:
@Agent
String myAgent(AgenticScope scope, String input) {
Object memoryId = scope.memoryId();
System.out.println("Processing for user: " + memoryId);
return "Result";
}Write state to the agentic scope for sharing between agents.
/**
* Write state with string key
* @param key State key
* @param value State value
*/
void writeState(String key, Object value);
/**
* Write state with typed key
* @param key Typed key class
* @param value State value
*/
<T> void writeState(Class<? extends TypedKey<T>> key, T value);
/**
* Write multiple state entries
* @param newState Map of state entries to write
*/
void writeStates(Map<String, Object> newState);Usage Examples:
@Agent
String processData(AgenticScope scope, String input) {
// Write with string key
scope.writeState("processed_data", processedResult);
scope.writeState("timestamp", System.currentTimeMillis());
// Write with typed key
scope.writeState(UserPreferences.KEY, new UserPreferences("dark", "en"));
// Write multiple entries
scope.writeStates(Map.of(
"status", "complete",
"count", 42,
"validated", true
));
return "Processing complete";
}Check if state exists in the agentic scope.
/**
* Check if state exists for string key
* @param key State key
* @return true if state exists
*/
boolean hasState(String key);
/**
* Check if state exists for typed key
* @param key Typed key class
* @return true if state exists
*/
boolean hasState(Class<? extends TypedKey<?>> key);Usage Examples:
@Agent
String conditionalAgent(AgenticScope scope, String input) {
// Check with string key
if (scope.hasState("user_preferences")) {
Object prefs = scope.readState("user_preferences");
// Use preferences
} else {
// Use defaults
}
// Check with typed key
if (scope.hasState(CachedData.KEY)) {
CachedData data = scope.readState(CachedData.KEY);
return "Using cached data: " + data;
}
return "No cached data available";
}Read state from the agentic scope.
/**
* Read state with string key
* @param key State key
* @return State value or null if not found
*/
Object readState(String key);
/**
* Read state with default value
* @param key State key
* @param defaultValue Default value if state doesn't exist
* @return State value or default
*/
<T> T readState(String key, T defaultValue);
/**
* Read state with typed key
* @param key Typed key class
* @return State value or null if not found
*/
<T> T readState(Class<? extends TypedKey<T>> key);
/**
* Get all state as map
* @return Map of all state entries
*/
Map<String, Object> state();Usage Examples:
@Agent
String analyzer(AgenticScope scope, String input) {
// Read with string key
String data = (String) scope.readState("fetched_data");
// Read with default value
int maxRetries = scope.readState("max_retries", 3);
String mode = scope.readState("mode", "standard");
// Read with typed key
UserProfile profile = scope.readState(UserProfile.KEY);
// Get all state
Map<String, Object> allState = scope.state();
System.out.println("Current state: " + allState);
return "Analysis complete";
}Type-safe keys for accessing agent state.
/**
* Type-safe key for accessing agent state
*/
interface TypedKey<T> {
/**
* Get key name
* @return Key name
*/
String name();
}Usage Examples:
import dev.langchain4j.agentic.declarative.TypedKey;
import dev.langchain4j.agentic.declarative.K;
// Define typed keys
class UserProfile {
static final Class<? extends TypedKey<UserProfile>> KEY = K.key("user_profile");
private String name;
private String email;
// Constructor, getters, setters
}
class ProcessingConfig {
static final Class<? extends TypedKey<ProcessingConfig>> KEY = K.key("config");
private int maxIterations;
private boolean strictMode;
// Constructor, getters, setters
}
@Agent
String typedAgent(AgenticScope scope, String input) {
// Write with typed key
UserProfile profile = new UserProfile("Alice", "alice@example.com");
scope.writeState(UserProfile.KEY, profile);
// Read with typed key (type-safe, no casting needed)
UserProfile retrieved = scope.readState(UserProfile.KEY);
System.out.println("Processing for: " + retrieved.getName());
// Check with typed key
if (scope.hasState(ProcessingConfig.KEY)) {
ProcessingConfig config = scope.readState(ProcessingConfig.KEY);
// Use config
}
return "Complete";
}
// Use in annotations
@Agent(
name = "processor",
typedOutputKey = UserProfile.KEY
)
String process(String input);Get context from other agents' invocations formatted as conversation.
/**
* Get context as conversation string for specified agent names
* @param agentNames Agent names to include
* @return Formatted conversation string
*/
String contextAsConversation(String... agentNames);
/**
* Get context as conversation for agent objects
* @param agents Agent objects to include
* @return Formatted conversation string
*/
String contextAsConversation(Object... agents);Usage Examples:
@Agent
String summarizer(AgenticScope scope, String input) {
// Get conversation from specific agents
String researchContext = scope.contextAsConversation("researcher", "fact-checker");
// Use as context for LLM
String prompt = "Based on this research:\n" + researchContext +
"\nProvide a summary.";
return callLLM(prompt);
}
@Agent
String reviewer(AgenticScope scope, String input) {
// Get conversation from multiple agents
String fullContext = scope.contextAsConversation(
"data-collector",
"analyzer",
"validator"
);
return "Review based on: " + fullContext;
}Access history of agent invocations within the agentic scope.
/**
* Get all agent invocations
* @return List of all agent invocations
*/
List<AgentInvocation> agentInvocations();
/**
* Get invocations for specific agent name
* @param agentName Agent name to filter by
* @return List of invocations for the agent
*/
List<AgentInvocation> agentInvocations(String agentName);
/**
* Get invocations for specific agent type
* @param agentType Agent class to filter by
* @return List of invocations for the agent type
*/
List<AgentInvocation> agentInvocations(Class<?> agentType);Usage Examples:
@Agent
String coordinator(AgenticScope scope, String input) {
// Get all invocations
List<AgentInvocation> allInvocations = scope.agentInvocations();
System.out.println("Total invocations: " + allInvocations.size());
// Get invocations for specific agent
List<AgentInvocation> researchInvocations = scope.agentInvocations("researcher");
System.out.println("Researcher called " + researchInvocations.size() + " times");
// Get invocations by type
List<AgentInvocation> dataAgentInvocations = scope.agentInvocations(DataAgent.class);
// Analyze invocation history
for (AgentInvocation invocation : allInvocations) {
System.out.println("Agent: " + invocation.agentName());
System.out.println("Input: " + invocation.input());
System.out.println("Output: " + invocation.output());
}
return "Coordination complete";
}Record of an individual agent invocation.
/**
* Record of an individual agent invocation
*/
record AgentInvocation(
Class<?> agentType,
String agentName,
String agentId,
Object input,
Object output
) {}Usage Examples:
@Agent
String auditor(AgenticScope scope, String input) {
List<AgentInvocation> invocations = scope.agentInvocations();
// Build audit trail
StringBuilder auditLog = new StringBuilder("Audit Trail:\n");
for (AgentInvocation inv : invocations) {
auditLog.append(String.format(
"Agent: %s (ID: %s)\n" +
"Type: %s\n" +
"Input: %s\n" +
"Output: %s\n\n",
inv.agentName(),
inv.agentId(),
inv.agentType().getSimpleName(),
inv.input(),
inv.output()
));
}
return auditLog.toString();
}
@Agent
String performanceAnalyzer(AgenticScope scope, String input) {
// Analyze which agents were most frequently called
Map<String, Long> invocationCounts = scope.agentInvocations().stream()
.collect(Collectors.groupingBy(
AgentInvocation::agentName,
Collectors.counting()
));
return "Invocation counts: " + invocationCounts;
}Retrieve the agentic scope after agent invocation.
/**
* Invoke and return scope
* @param input Input string
* @return Result with agentic scope
*/
ResultWithAgenticScope<Object> invokeWithAgenticScope(String input);
/**
* Invoke with memory ID and return scope
* @param memoryId Memory ID
* @param input Input string
* @return Result with agentic scope
*/
ResultWithAgenticScope<Object> invokeWithAgenticScope(Object memoryId, String input);
/**
* Invoke with args and return scope
* @param memoryId Memory ID
* @param input Input string
* @param args Additional arguments
* @return Result with agentic scope
*/
ResultWithAgenticScope<Object> invokeWithAgenticScope(Object memoryId, String input, Object... args);
/**
* Invoke with map and return scope
* @param arguments Argument map
* @return Result with agentic scope
*/
ResultWithAgenticScope<Object> invokeWithAgenticScope(Map<String, Object> arguments);
/**
* Invoke with memory ID, map and return scope
* @param memoryId Memory ID
* @param arguments Argument map
* @return Result with agentic scope
*/
ResultWithAgenticScope<Object> invokeWithAgenticScope(Object memoryId, Map<String, Object> arguments);Usage Examples:
import dev.langchain4j.agentic.scope.ResultWithAgenticScope;
UntypedAgent agent = AgenticServices.agentBuilder()
.chatModel(chatModel)
.build();
// Invoke and get scope
ResultWithAgenticScope<Object> result = agent.invokeWithAgenticScope("Process this");
// Access result
Object output = result.result();
System.out.println("Result: " + output);
// Access scope
AgenticScope scope = result.agenticScope();
Map<String, Object> state = scope.state();
List<AgentInvocation> invocations = scope.agentInvocations();
System.out.println("Final state: " + state);
System.out.println("Total invocations: " + invocations.size());
// With memory ID
ResultWithAgenticScope<Object> result2 = agent.invokeWithAgenticScope(
"user-123",
"Process this"
);
// With arguments map
ResultWithAgenticScope<Object> result3 = agent.invokeWithAgenticScope(
Map.of("input", "data", "mode", "fast")
);Passing data through sequential stages:
// Agent 1: Fetch data
@Agent(name = "fetcher", outputKey = "raw_data")
String fetch(String source) {
return fetchFromSource(source);
}
// Agent 2: Transform data
@Agent(name = "transformer", outputKey = "transformed_data")
String transform(AgenticScope scope) {
String rawData = (String) scope.readState("raw_data");
return transformData(rawData);
}
// Agent 3: Validate data
@Agent(name = "validator", outputKey = "validation_status")
boolean validate(AgenticScope scope) {
String data = (String) scope.readState("transformed_data");
return isValid(data);
}
// Agent 4: Store data (conditional on validation)
@Agent(name = "storer")
String store(AgenticScope scope) {
boolean isValid = scope.readState("validation_status", false);
if (isValid) {
String data = (String) scope.readState("transformed_data");
storeData(data);
return "Stored successfully";
} else {
return "Validation failed, data not stored";
}
}Sharing configuration across agents:
@Agent
String configuredAgent(AgenticScope scope, String input) {
// Initialize default configuration
if (!scope.hasState("config")) {
Map<String, Object> defaultConfig = Map.of(
"max_retries", 3,
"timeout", 30000,
"strict_mode", false
);
scope.writeStates(defaultConfig);
}
// Read configuration
int maxRetries = scope.readState("max_retries", 3);
int timeout = scope.readState("timeout", 30000);
boolean strictMode = scope.readState("strict_mode", false);
// Process with configuration
return processWithConfig(input, maxRetries, timeout, strictMode);
}Building up results over multiple agent invocations:
@Agent
String accumulator(AgenticScope scope, String input) {
// Get or initialize accumulator
List<String> results = (List<String>) scope.readState("accumulated_results");
if (results == null) {
results = new ArrayList<>();
}
// Add new result
String result = processInput(input);
results.add(result);
// Store updated accumulator
scope.writeState("accumulated_results", results);
// Increment counter
int count = scope.readState("process_count", 0);
scope.writeState("process_count", count + 1);
return "Processed item " + count;
}Using state for dynamic feature control:
@Agent
String featureFlaggedAgent(AgenticScope scope, String input) {
// Check feature flags
boolean useNewAlgorithm = scope.readState("feature.new_algorithm", false);
boolean enableCaching = scope.readState("feature.caching", true);
boolean debugMode = scope.readState("feature.debug", false);
if (debugMode) {
System.out.println("Debug: Processing input: " + input);
}
if (enableCaching && scope.hasState("cached_result")) {
return (String) scope.readState("cached_result");
}
String result = useNewAlgorithm ?
processWithNewAlgorithm(input) :
processWithLegacyAlgorithm(input);
if (enableCaching) {
scope.writeState("cached_result", result);
}
return result;
}Tracking and recovering from errors:
@Agent
String errorTrackingAgent(AgenticScope scope, String input) {
try {
String result = riskyOperation(input);
// Clear any previous errors
scope.writeState("has_error", false);
scope.writeState("error_message", null);
return result;
} catch (Exception e) {
// Track error state
scope.writeState("has_error", true);
scope.writeState("error_message", e.getMessage());
scope.writeState("error_timestamp", System.currentTimeMillis());
// Increment error counter
int errorCount = scope.readState("error_count", 0);
scope.writeState("error_count", errorCount + 1);
throw new RuntimeException("Operation failed", e);
}
}
@Agent
String errorRecoveryAgent(AgenticScope scope, String input) {
if (scope.hasState("has_error") && scope.readState("has_error", false)) {
String errorMsg = (String) scope.readState("error_message");
int errorCount = scope.readState("error_count", 0);
if (errorCount < 3) {
return "Attempting recovery from: " + errorMsg;
} else {
return "Too many errors, giving up";
}
}
return "No errors to recover from";
}Using strongly-typed state classes:
// Define state classes
class ProcessingMetrics {
static final Class<? extends TypedKey<ProcessingMetrics>> KEY = K.key("metrics");
private int itemsProcessed;
private long totalTime;
private List<String> errors;
public ProcessingMetrics() {
this.itemsProcessed = 0;
this.totalTime = 0;
this.errors = new ArrayList<>();
}
// Getters and setters
}
class UserContext {
static final Class<? extends TypedKey<UserContext>> KEY = K.key("user_context");
private String userId;
private String sessionId;
private Map<String, Object> preferences;
// Constructor, getters, setters
}
@Agent
String metricsAgent(AgenticScope scope, String input) {
// Get or create metrics
ProcessingMetrics metrics = scope.readState(ProcessingMetrics.KEY);
if (metrics == null) {
metrics = new ProcessingMetrics();
}
// Process and update metrics
long startTime = System.currentTimeMillis();
try {
String result = process(input);
metrics.setItemsProcessed(metrics.getItemsProcessed() + 1);
return result;
} catch (Exception e) {
metrics.getErrors().add(e.getMessage());
throw e;
} finally {
long elapsed = System.currentTimeMillis() - startTime;
metrics.setTotalTime(metrics.getTotalTime() + elapsed);
scope.writeState(ProcessingMetrics.KEY, metrics);
}
}
@Agent
String userContextAgent(AgenticScope scope, String input) {
// Read user context
UserContext context = scope.readState(UserContext.KEY);
if (context != null) {
String userId = context.getUserId();
Map<String, Object> prefs = context.getPreferences();
// Process with user context
return processForUser(input, userId, prefs);
}
return "No user context available";
}// Good
scope.writeState("user_validation_result", result);
scope.writeState("last_api_call_timestamp", timestamp);
// Avoid
scope.writeState("result", result);
scope.writeState("time", timestamp);// Prevent null pointer exceptions
int retries = scope.readState("retry_count", 0);
String mode = scope.readState("processing_mode", "standard");if (scope.hasState("required_config")) {
Config config = (Config) scope.readState("required_config");
// Use config
} else {
throw new IllegalStateException("Required configuration missing");
}// Prefer typed keys for type safety
class UserSession {
static final Class<? extends TypedKey<UserSession>> KEY = K.key("session");
// ...
}
UserSession session = scope.readState(UserSession.KEY); // Type-safe
// Instead of
UserSession session = (UserSession) scope.readState("session"); // Requires castUntypedAgent workflow = AgenticServices.sequenceBuilder()
.subAgents(agent1, agent2)
.beforeCall(scope -> {
// Initialize state before agents run
scope.writeState("workflow_id", UUID.randomUUID().toString());
scope.writeState("start_time", System.currentTimeMillis());
scope.writeState("error_count", 0);
})
.build();Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-agentic@1.11.0docs
declarative
A2AClientAgent
ActivationCondition
Agent
ConditionalAgent
ErrorHandler
ExitCondition
HumanInTheLoop
HumanInTheLoopResponseSupplier
LoopAgent
LoopCounter
Output
ParallelAgent
ParallelExecutor
PlannerAgent
SequenceAgent
SupervisorAgent
SupervisorRequest
quick-start
workflows