CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-agentic

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.

Overview
Eval results
Files

state-management.mddocs/api/

State Management API

Complete API reference for AgenticScope - shared state environment for agents within an agentic system.

Quick Start

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";
}

Core State Operations

Memory ID Access

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";
}

Writing State

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";
}

Checking State

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";
}

Reading State

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 State Access

TypedKey Interface

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

Context Sharing

Context as Conversation

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;
}

Invocation History

Agent Invocations

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";
}

AgentInvocation Record

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;
}

Invoking with AgenticScope

ResultWithAgenticScope

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

Common State Management Patterns

Pattern 1: Pipeline State

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";
    }
}

Pattern 2: Configuration State

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

Pattern 3: Accumulator State

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;
}

Pattern 4: Feature Flags

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;
}

Pattern 5: Error State Tracking

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";
}

Pattern 6: Typed State with Custom Classes

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";
}

Best Practices

1. Use Descriptive Keys

// Good
scope.writeState("user_validation_result", result);
scope.writeState("last_api_call_timestamp", timestamp);

// Avoid
scope.writeState("result", result);
scope.writeState("time", timestamp);

2. Provide Defaults When Reading

// Prevent null pointer exceptions
int retries = scope.readState("retry_count", 0);
String mode = scope.readState("processing_mode", "standard");

3. Check Before Reading Critical State

if (scope.hasState("required_config")) {
    Config config = (Config) scope.readState("required_config");
    // Use config
} else {
    throw new IllegalStateException("Required configuration missing");
}

4. Use TypedKeys for Complex Types

// 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 cast

5. Initialize State Early

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

See Also

  • Agent Builder API - Creating agents that use AgenticScope
  • Workflows Overview - Using AgenticScope across workflows
  • Observability API - Monitoring state changes

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-agentic@1.11.0

docs

index.md

tile.json