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

persistence.mddocs/advanced/

AgenticScope Persistence

Persist and restore AgenticScope state across application restarts for long-running workflows and stateful agent systems.

Overview

Persistence enables:

  • State survival across restarts
  • Long-running workflows
  • Resume interrupted workflows
  • Audit trails and history
  • Multi-session conversations

Lifecycle Modes

  1. Ephemeral - Scope exists only during execution (default)
  2. Registered - Scope persists in memory for application lifetime
  3. Persistent - Scope saved to storage, survives restarts

AgenticScopeStore Interface

interface AgenticScopeStore {
    void save(AgenticScopeKey key, DefaultAgenticScope scope);
    DefaultAgenticScope load(AgenticScopeKey key);
    void delete(AgenticScopeKey key);
    Set<AgenticScopeKey> getAllKeys();
}

Implement this interface to provide custom persistence backends.

AgenticScopePersister

enum AgenticScopePersister {
    INSTANCE;

    public void setStore(AgenticScopeStore store);
}

Configuration:

// Configure at application startup
AgenticScopeStore store = new RedisAgenticScopeStore(redisClient);
AgenticScopePersister.INSTANCE.setStore(store);

// Now all agents persist their scopes
UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .build();

// Scope persisted automatically
agent.invoke("user-123", "Hello");

// Later, scope restored
agent.invoke("user-123", "What did I say before?");

AgenticScopeKey

record AgenticScopeKey(
    String agentId,
    Object memoryId
) {}

Composite key identifying a persisted scope:

AgenticScopeKey key = new AgenticScopeKey("my-agent-id", "user-123");

DefaultAgenticScope scope = store.load(key);
if (scope != null) {
    System.out.println("State: " + scope.state());
}

store.delete(key);

Serialization

AgenticScopeSerializer

class AgenticScopeSerializer {
    public static String toJson(DefaultAgenticScope scope);
    public static DefaultAgenticScope fromJson(String json);
}

Usage:

// Serialize
DefaultAgenticScope scope = // ... get scope
String json = AgenticScopeSerializer.toJson(scope);

// Save to file
Files.writeString(Path.of("scope.json"), json);

// Load from file
String loadedJson = Files.readString(Path.of("scope.json"));
DefaultAgenticScope restoredScope = AgenticScopeSerializer.fromJson(loadedJson);

// Access restored state
Map<String, Object> state = restoredScope.state();

Storage Implementations

File-Based Store

class FileSystemAgenticScopeStore implements AgenticScopeStore {
    private final Path storageDir;

    public FileSystemAgenticScopeStore(Path storageDir) {
        this.storageDir = storageDir;
        Files.createDirectories(storageDir);
    }

    @Override
    public void save(AgenticScopeKey key, DefaultAgenticScope scope) {
        String json = AgenticScopeSerializer.toJson(scope);
        Path file = getFilePath(key);
        Files.writeString(file, json);
    }

    @Override
    public DefaultAgenticScope load(AgenticScopeKey key) {
        Path file = getFilePath(key);
        if (!Files.exists(file)) return null;

        String json = Files.readString(file);
        return AgenticScopeSerializer.fromJson(json);
    }

    @Override
    public void delete(AgenticScopeKey key) {
        Files.deleteIfExists(getFilePath(key));
    }

    @Override
    public Set<AgenticScopeKey> getAllKeys() {
        return Files.list(storageDir)
            .filter(path -> path.toString().endsWith(".json"))
            .map(this::pathToKey)
            .collect(Collectors.toSet());
    }

    private Path getFilePath(AgenticScopeKey key) {
        String fileName = key.agentId() + "_" + key.memoryId() + ".json";
        return storageDir.resolve(fileName);
    }

    private AgenticScopeKey pathToKey(Path path) {
        String fileName = path.getFileName().toString().replace(".json", "");
        String[] parts = fileName.split("_", 2);
        return new AgenticScopeKey(parts[0], parts[1]);
    }
}

// Configure
AgenticScopePersister.INSTANCE.setStore(
    new FileSystemAgenticScopeStore(Path.of("./scope-storage"))
);

Redis Store

class RedisAgenticScopeStore implements AgenticScopeStore {
    private final RedisClient redis;

    public RedisAgenticScopeStore(RedisClient redis) {
        this.redis = redis;
    }

    @Override
    public void save(AgenticScopeKey key, DefaultAgenticScope scope) {
        String json = AgenticScopeSerializer.toJson(scope);
        redis.set(keyToString(key), json);
    }

    @Override
    public DefaultAgenticScope load(AgenticScopeKey key) {
        String json = redis.get(keyToString(key));
        return json != null ? AgenticScopeSerializer.fromJson(json) : null;
    }

    @Override
    public void delete(AgenticScopeKey key) {
        redis.del(keyToString(key));
    }

    @Override
    public Set<AgenticScopeKey> getAllKeys() {
        return redis.keys("agentic:scope:*").stream()
            .map(this::stringToKey)
            .collect(Collectors.toSet());
    }

    private String keyToString(AgenticScopeKey key) {
        return "agentic:scope:" + key.agentId() + ":" + key.memoryId();
    }

    private AgenticScopeKey stringToKey(String str) {
        String[] parts = str.replace("agentic:scope:", "").split(":");
        return new AgenticScopeKey(parts[0], parts[1]);
    }
}

Database Store

class DatabaseAgenticScopeStore implements AgenticScopeStore {
    private final DataSource dataSource;

    public DatabaseAgenticScopeStore(DataSource dataSource) {
        this.dataSource = dataSource;
        initializeSchema();
    }

    private void initializeSchema() {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("""
                CREATE TABLE IF NOT EXISTS agentic_scopes (
                    agent_id VARCHAR(255) NOT NULL,
                    memory_id VARCHAR(255) NOT NULL,
                    scope_data TEXT NOT NULL,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (agent_id, memory_id)
                )
            """);
        }
    }

    @Override
    public void save(AgenticScopeKey key, DefaultAgenticScope scope) {
        String json = AgenticScopeSerializer.toJson(scope);
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("""
                INSERT INTO agentic_scopes (agent_id, memory_id, scope_data)
                VALUES (?, ?, ?)
                ON CONFLICT (agent_id, memory_id)
                DO UPDATE SET scope_data = EXCLUDED.scope_data,
                              updated_at = CURRENT_TIMESTAMP
            """)) {
            stmt.setString(1, key.agentId());
            stmt.setString(2, key.memoryId().toString());
            stmt.setString(3, json);
            stmt.executeUpdate();
        }
    }

    @Override
    public DefaultAgenticScope load(AgenticScopeKey key) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("""
                SELECT scope_data FROM agentic_scopes
                WHERE agent_id = ? AND memory_id = ?
            """)) {
            stmt.setString(1, key.agentId());
            stmt.setString(2, key.memoryId().toString());
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                String json = rs.getString("scope_data");
                return AgenticScopeSerializer.fromJson(json);
            }
            return null;
        }
    }

    @Override
    public void delete(AgenticScopeKey key) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("""
                DELETE FROM agentic_scopes
                WHERE agent_id = ? AND memory_id = ?
            """)) {
            stmt.setString(1, key.agentId());
            stmt.setString(2, key.memoryId().toString());
            stmt.executeUpdate();
        }
    }

    @Override
    public Set<AgenticScopeKey> getAllKeys() {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("""
                SELECT agent_id, memory_id FROM agentic_scopes
            """)) {
            Set<AgenticScopeKey> keys = new HashSet<>();
            while (rs.next()) {
                keys.add(new AgenticScopeKey(
                    rs.getString("agent_id"),
                    rs.getString("memory_id")
                ));
            }
            return keys;
        }
    }
}

AgenticScopeAccess

interface AgenticScopeAccess {
    AgenticScope getAgenticScope(Object memoryId);
    void evictAgenticScope(Object memoryId);
}

Usage:

// UntypedAgent implements AgenticScopeAccess
UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .build();

// Access scope directly
AgenticScope scope = agent.getAgenticScope("user-123");
if (scope != null) {
    Map<String, Object> state = scope.state();
    System.out.println("Current state: " + state);
}

// Evict scope
agent.evictAgenticScope("user-123");

Management Utilities

Cleanup Old Scopes

class ScopeCleanupJob {
    private final AgenticScopeStore store;

    public void cleanupOldScopes(Duration maxAge) {
        Set<AgenticScopeKey> allKeys = store.getAllKeys();
        for (AgenticScopeKey key : allKeys) {
            DefaultAgenticScope scope = store.load(key);
            if (scope != null && isOlderThan(scope, maxAge)) {
                store.delete(key);
            }
        }
    }

    private boolean isOlderThan(DefaultAgenticScope scope, Duration maxAge) {
        // Check scope metadata for age
        return false; // Implementation depends on metadata
    }
}

Backup and Restore

class ScopeBackup {
    private final AgenticScopeStore store;

    public void backupAllScopes(Path backupDir) throws IOException {
        Files.createDirectories(backupDir);
        Set<AgenticScopeKey> keys = store.getAllKeys();

        for (AgenticScopeKey key : keys) {
            DefaultAgenticScope scope = store.load(key);
            if (scope != null) {
                String json = AgenticScopeSerializer.toJson(scope);
                String fileName = key.agentId() + "_" + key.memoryId() + ".json";
                Files.writeString(backupDir.resolve(fileName), json);
            }
        }
    }

    public void restoreFromBackup(Path backupDir) throws IOException {
        Files.list(backupDir)
            .filter(path -> path.toString().endsWith(".json"))
            .forEach(path -> {
                try {
                    String json = Files.readString(path);
                    DefaultAgenticScope scope = AgenticScopeSerializer.fromJson(json);

                    String fileName = path.getFileName().toString().replace(".json", "");
                    String[] parts = fileName.split("_", 2);
                    AgenticScopeKey key = new AgenticScopeKey(parts[0], parts[1]);

                    store.save(key, scope);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to restore scope", e);
                }
            });
    }
}

Custom JSON Codec

interface AgenticScopeJsonCodec {
    DefaultAgenticScope fromJson(String json);
    String toJson(DefaultAgenticScope scope);
}

Example with Gson:

class GsonAgenticScopeCodec implements AgenticScopeJsonCodec {
    private final Gson gson = new Gson();

    @Override
    public DefaultAgenticScope fromJson(String json) {
        return gson.fromJson(json, DefaultAgenticScope.class);
    }

    @Override
    public String toJson(DefaultAgenticScope scope) {
        return gson.toJson(scope);
    }
}

// Register via ServiceLoader
// Create META-INF/services/dev.langchain4j.agentic.scope.persistence.AgenticScopeJsonCodec
// Add: com.mycompany.GsonAgenticScopeCodec

Complete Example

public class PersistentAgentApp {
    public static void main(String[] args) {
        // 1. Configure persistence
        AgenticScopeStore store = new DatabaseAgenticScopeStore(dataSource);
        AgenticScopePersister.INSTANCE.setStore(store);

        // 2. Create agent
        UntypedAgent agent = AgenticServices.agentBuilder()
            .chatModel(chatModel)
            .name("persistent-assistant")
            .build();

        // 3. First conversation
        String userId = "user-123";
        String response1 = agent.invoke(userId, "My name is Alice").toString();
        System.out.println(response1);

        // 4. Simulate restart (scope persisted to storage)
        // ... application restarts ...

        // 5. Continue conversation
        String response2 = agent.invoke(userId, "What is my name?").toString();
        System.out.println(response2); // Should remember "Alice"

        // 6. Access scope directly
        AgenticScope scope = agent.getAgenticScope(userId);
        System.out.println("Conversation history: " + scope.agentInvocations());

        // 7. Cleanup when done
        agent.evictAgenticScope(userId);
    }
}

Best Practices

  1. Choose appropriate storage - File for dev, Redis/DB for production
  2. Implement cleanup - Remove old scopes periodically
  3. Handle serialization errors - Gracefully handle corrupt data
  4. Secure sensitive data - Encrypt scope data if needed
  5. Monitor storage size - Track growth of persisted scopes
  6. Test restore paths - Ensure scopes can be recovered
  7. Version your schema - Plan for scope format changes
  8. Backup regularly - Don't lose important state

See Also

  • State Management API - State management with AgenticScope
  • Error Handling - Handling persistence errors
  • Sequential Workflows - Using persistence in workflows

Install with Tessl CLI

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

docs

advanced

a2a.md

error-handling.md

human-in-loop.md

persistence.md

index.md

tile.json