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

typed-keys.mddocs/declarative/

TypedKey Usage Patterns

Type-safe keys for accessing AgenticScope state with compile-time type checking.

Overview

TypedKey enables:

  • Compile-time type safety for state access
  • Self-documenting state keys
  • Prevention of type casting errors
  • Clean state key management

TypedKey Interface

interface TypedKey<T> {
    String name();
}

Defining TypedKeys

Interface-Based Keys

interface UserName extends TypedKey<String> {
    @Override
    default String name() {
        return "UserName";
    }
}

interface UserAge extends TypedKey<Integer> {
    @Override
    default String name() {
        return "UserAge";
    }
}

interface UserProfile extends TypedKey<Profile> {
    @Override
    default String name() {
        return "UserProfile";
    }
}

Using K Factory

class K {
    static <T> Class<? extends TypedKey<T>> key(String name);
}

Example:

import dev.langchain4j.agentic.declarative.K;

Class<? extends TypedKey<Integer>> countKey = K.key("count");
Class<? extends TypedKey<String>> resultKey = K.key("result");

Using TypedKeys in Agents

With @Agent Annotation

interface TypedAgent {
    @Agent(
        name = "counter",
        typedOutputKey = CountKey.class
    )
    Integer count(String input);
}

interface CountKey extends TypedKey<Integer> {
    @Override
    default String name() {
        return "count";
    }
}

With Workflow Annotations

interface TypedWorkflow {
    @SequenceAgent(
        name = "typed-pipeline",
        typedOutputKey = ResultKey.class,
        subAgents = {Processor1.class, Processor2.class}
    )
    String process(String input);
}

interface ResultKey extends TypedKey<String> {
    @Override
    default String name() {
        return "final_result";
    }
}

Reading State with TypedKeys

Direct Scope Access

interface UserDataKey extends TypedKey<UserData> {
    @Override
    default String name() {
        return "user_data";
    }
}

// In agent method
@Agent(name = "processor")
String process(AgenticScope scope) {
    // Type-safe read
    UserData data = scope.readState(UserDataKey.class);

    // No casting needed!
    String name = data.getName();
    int age = data.getAge();

    return "Processed: " + name;
}

With Default Values

interface ScoreKey extends TypedKey<Double> {
    @Override
    default String name() {
        return "score";
    }
}

@Agent(name = "evaluator")
String evaluate(AgenticScope scope) {
    // Read with default value
    Double score = scope.readState(ScoreKey.class, 0.0);

    return score >= 0.8 ? "Pass" : "Fail";
}

Parameter Injection with @K

Example:

interface UserName extends TypedKey<String> {
    @Override
    default String name() {
        return "UserName";
    }
}

interface UserAge extends TypedKey<Integer> {
    @Override
    default String name() {
        return "UserAge";
    }
}

interface PersonalizedAgent {
    // Parameters injected from AgenticScope
    @Agent(name = "greeter")
    String greet(@K(UserName.class) String name, @K(UserAge.class) int age);
}

// Usage
PersonalizedAgent agent = AgenticServices.createAgenticSystem(
    PersonalizedAgent.class,
    chatModel
);

// Set state
AgenticScope scope = new AgenticScope();
scope.writeState(new UserName(), "Alice");
scope.writeState(new UserAge(), 30);

// Parameters automatically injected
String greeting = agent.greet("", 0); // Actual values from scope

Organizing TypedKeys

Centralized Key Registry

public final class StateKeys {
    private StateKeys() {}

    // User keys
    public interface UserId extends TypedKey<String> {
        @Override
        default String name() { return "user_id"; }
    }

    public interface UserProfile extends TypedKey<Profile> {
        @Override
        default String name() { return "user_profile"; }
    }

    // Processing keys
    public interface RawData extends TypedKey<String> {
        @Override
        default String name() { return "raw_data"; }
    }

    public interface ProcessedData extends TypedKey<ProcessedResult> {
        @Override
        default String name() { return "processed_data"; }
    }

    // Result keys
    public interface FinalResult extends TypedKey<String> {
        @Override
        default String name() { return "final_result"; }
    }

    public interface Metadata extends TypedKey<Map<String, Object>> {
        @Override
        default String name() { return "metadata"; }
    }
}

// Usage
@Agent(name = "processor", typedOutputKey = StateKeys.ProcessedData.class)
ProcessedResult process(AgenticScope scope) {
    String raw = scope.readState(StateKeys.RawData.class);
    return processRawData(raw);
}

Domain-Specific Key Groups

public final class OrderKeys {
    public interface OrderId extends TypedKey<String> {
        @Override default String name() { return "order_id"; }
    }

    public interface OrderItems extends TypedKey<List<Item>> {
        @Override default String name() { return "order_items"; }
    }

    public interface OrderTotal extends TypedKey<BigDecimal> {
        @Override default String name() { return "order_total"; }
    }
}

public final class CustomerKeys {
    public interface CustomerId extends TypedKey<String> {
        @Override default String name() { return "customer_id"; }
    }

    public interface CustomerTier extends TypedKey<Tier> {
        @Override default String name() { return "customer_tier"; }
    }
}

Complex Type Examples

List Types

interface TaskList extends TypedKey<List<Task>> {
    @Override
    default String name() {
        return "task_list";
    }
}

@Agent(name = "task-processor", typedOutputKey = TaskList.class)
List<Task> processTasks(AgenticScope scope) {
    List<Task> tasks = scope.readState(TaskList.class, Collections.emptyList());

    return tasks.stream()
        .filter(Task::isActive)
        .collect(Collectors.toList());
}

Map Types

interface ConfigMap extends TypedKey<Map<String, String>> {
    @Override
    default String name() {
        return "config";
    }
}

@Agent(name = "configurator")
String configure(AgenticScope scope) {
    Map<String, String> config = scope.readState(
        ConfigMap.class,
        Collections.emptyMap()
    );

    String apiUrl = config.get("api_url");
    String apiKey = config.get("api_key");

    return "Configured: " + apiUrl;
}

Custom Domain Objects

interface AnalysisResult extends TypedKey<Analysis> {
    @Override
    default String name() {
        return "analysis_result";
    }
}

class Analysis {
    private final String summary;
    private final double confidence;
    private final List<Finding> findings;

    // Constructor, getters, etc.
}

@Agent(name = "analyzer", typedOutputKey = AnalysisResult.class)
Analysis analyze(AgenticScope scope) {
    String data = scope.readState("raw_data", String.class);

    return new Analysis(
        generateSummary(data),
        calculateConfidence(data),
        extractFindings(data)
    );
}

@Agent(name = "reporter")
String generateReport(AgenticScope scope) {
    Analysis analysis = scope.readState(AnalysisResult.class);

    return String.format(
        "Summary: %s\nConfidence: %.2f\nFindings: %d",
        analysis.getSummary(),
        analysis.getConfidence(),
        analysis.getFindings().size()
    );
}

LoopCounter Utility

class LoopCounter {
    static int get(AgenticScope scope);
    static TypedKey<Integer> key();
}

Example:

interface LoopWorkflow {
    @LoopAgent(maxIterations = 10, subAgents = {Worker.class})
    String work(String input);

    @ExitCondition
    boolean shouldExit(AgenticScope scope) {
        int iteration = LoopCounter.get(scope);
        double quality = scope.readState("quality", 0.0);

        return quality >= 0.95 || iteration >= 8;
    }
}

Best Practices

1. Use Descriptive Names

// Good
interface CustomerEmailAddress extends TypedKey<String> {
    @Override default String name() { return "customer_email"; }
}

// Avoid
interface Email extends TypedKey<String> {
    @Override default String name() { return "e"; }
}

2. Group Related Keys

public final class PaymentKeys {
    public interface Amount extends TypedKey<BigDecimal> {
        @Override default String name() { return "payment_amount"; }
    }

    public interface Method extends TypedKey<PaymentMethod> {
        @Override default String name() { return "payment_method"; }
    }

    public interface Status extends TypedKey<PaymentStatus> {
        @Override default String name() { return "payment_status"; }
    }
}

3. Document Complex Types

/**
 * Comprehensive user profile including preferences and history
 */
interface UserProfile extends TypedKey<Profile> {
    @Override
    default String name() {
        return "user_profile";
    }
}

4. Use Consistent Naming

// Consistent pattern for all keys
interface InputData extends TypedKey<String> {
    @Override default String name() { return "input_data"; }
}

interface ProcessedData extends TypedKey<ProcessedResult> {
    @Override default String name() { return "processed_data"; }
}

interface ValidatedData extends TypedKey<ValidationResult> {
    @Override default String name() { return "validated_data"; }
}

Complete Example

// Define keys
public final class WorkflowKeys {
    public interface InputText extends TypedKey<String> {
        @Override default String name() { return "input_text"; }
    }

    public interface Analysis extends TypedKey<AnalysisResult> {
        @Override default String name() { return "analysis"; }
    }

    public interface Summary extends TypedKey<String> {
        @Override default String name() { return "summary"; }
    }

    public interface Confidence extends TypedKey<Double> {
        @Override default String name() { return "confidence"; }
    }
}

// Use in workflow
interface TextAnalysisWorkflow {
    @SequenceAgent(
        name = "analysis-workflow",
        typedOutputKey = WorkflowKeys.Summary.class,
        subAgents = {Analyzer.class, Summarizer.class}
    )
    String analyzeText(String input);
}

interface Analyzer {
    @Agent(name = "analyzer", typedOutputKey = WorkflowKeys.Analysis.class)
    AnalysisResult analyze(
        @K(WorkflowKeys.InputText.class) String text
    );
}

interface Summarizer {
    @Agent(name = "summarizer", typedOutputKey = WorkflowKeys.Summary.class)
    String summarize(AgenticScope scope) {
        AnalysisResult analysis = scope.readState(WorkflowKeys.Analysis.class);
        Double confidence = scope.readState(WorkflowKeys.Confidence.class, 0.0);

        return String.format(
            "Analysis: %s (Confidence: %.2f)",
            analysis.getSummary(),
            confidence
        );
    }
}

See Also

  • Declarative Annotations - All declarative annotations
  • State Management API - State management with AgenticScope
  • Loop Workflows - LoopCounter usage

Install with Tessl CLI

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

docs

declarative

annotations.md

typed-keys.md

index.md

tile.json