CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-agentic

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.

Overview
Eval results
Files

scope-state.mddocs/

Shared State and Scope

This document describes how to access and manipulate shared state across agents in a workflow using AgenticScope.

Capabilities

Agentic Scope Interface

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

Result With Agentic Scope

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

Agentic Scope Access Interface

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

Chat Messages Access Interface

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

State Management Patterns

Passing Data Between Sequential Agents

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

Manual State Management in Error Handler

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

Conditional Logic Based on State

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

Loop State Tracking

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

Accessing State After Workflow Completion

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

Type-Safe State Access

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 errors

Type Safety Guidelines:

  • Recommended: Always use readState(key, defaultValue) overload - the default value provides type safety
  • Alternative: Cast the result explicitly: (String) scope.readState(key)
  • Avoid: Assigning readState(key) without casting will cause type mismatch errors
  • Note: The return type is Object, so Java cannot infer the type automatically

State Lifecycle

The agentic scope lifecycle in multi-agent workflows:

  1. Initialization: Scope created when workflow starts
  2. Input Binding: Initial @V parameters written to scope
  3. Agent Execution: Each agent can read from and write to scope
  4. Output Storage: Agent outputs stored with their outputKey
  5. Variable Passing: Subsequent agents access previous outputs via @V parameters
  6. Completion: Scope remains accessible via ResultWithAgenticScope
  7. Disposal: Scope lifecycle tied to workflow execution context

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 history

Install with Tessl CLI

npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-agentic@1.7.0

docs

agent-definition.md

error-handling.md

index.md

lifecycle-control.md

memory-parameters.md

multi-agent-orchestration.md

resource-suppliers.md

runtime-support.md

scope-state.md

tile.json