java langgraph-checkpoint

This commit is contained in:
Nuno Campos
2025-03-01 18:51:45 -08:00
parent c4275bdc32
commit 196bcfe08d
24 changed files with 2321 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
##############################
## Java
##############################
.mtj.tmp/
*.class
*.jar
*.war
*.ear
*.nar
hs_err_pid*
replay_pid*
##############################
## Maven
##############################
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
pom.xml.bak
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
##############################
## Gradle
##############################
bin/
build/
.gradle
.gradletasknamecache
gradle-app.setting
!gradle-wrapper.jar
##############################
## IntelliJ
##############################
out/
.idea/
.idea_modules/
*.iml
*.ipr
*.iws
##############################
## Eclipse
##############################
.settings/
bin/
tmp/
.metadata
.classpath
.project
*.tmp
*.bak
*.swp
*~.nib
local.properties
.loadpath
.factorypath
##############################
## NetBeans
##############################
nbproject/private/
build/
nbbuild/
dist/
nbdist/
nbactions.xml
nb-configuration.xml
##############################
## Visual Studio Code
##############################
.vscode/
.code-workspace
##############################
## OS X
##############################
.DS_Store
##############################
## Miscellaneous
##############################
*.log
+63
View File
@@ -0,0 +1,63 @@
# LangGraph Java
A Java port of LangGraph, a framework for building stateful, observable applications with large language models (LLMs).
## Project Structure
- `langgraph-checkpoint`: Base persistence interfaces
- `langgraph-core`: Main library with channels, Pregel, and StateGraph
- `langgraph-examples`: Example applications
## Requirements
- Java 17 or higher
- Gradle 7.0 or higher
## Building
```bash
./gradlew build
```
## Features
- Graph-based architecture with nodes and edges
- Type-safe state schema using Java Records
- Cyclical execution patterns
- Checkpoint integration for persistence
- Streaming support
- Human-in-the-loop capabilities
## Usage Example
```java
// Create a graph with our schema
StateGraph<CounterState> graph = new StateGraph<>(CounterState.class);
// Add nodes
graph.addNode("increment", state -> {
Map<String, Object> updates = new HashMap<>();
updates.put("count", state.count() + 1);
return updates;
});
// Add edges
graph.addEdge(START, "increment");
graph.addEdge("increment", "check");
// Add conditional edge
graph.addConditionalEdges("check", state -> {
if (state.count() >= 3) {
return "finish";
}
return "increment";
});
// Compile and run
CompiledStateGraph<CounterState> compiled = graph.compile();
CounterState result = compiled.invoke(new CounterState(0, ""));
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.
+39
View File
@@ -0,0 +1,39 @@
plugins {
id 'java-library'
}
allprojects {
group = 'com.langgraph'
version = '0.1.0-SNAPSHOT'
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java-library'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters'
}
dependencies {
// Testing dependencies
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testImplementation 'org.mockito:mockito-core:5.2.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
}
test {
useJUnitPlatform()
}
}
Binary file not shown.
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,5 @@
dependencies {
// MessagePack for serialization
implementation 'org.msgpack:msgpack-core:0.9.3'
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.3'
}
@@ -0,0 +1,60 @@
package com.langgraph.checkpoint.base;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Asynchronous interface for saving and loading checkpoints.
*/
public interface AsyncBaseCheckpointSaver {
/**
* Create a new checkpoint asynchronously.
*
* @param threadId The ID of the thread to checkpoint
* @param channelValues The values of the channels to checkpoint
* @return CompletableFuture with the ID of the new checkpoint
*/
CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues);
/**
* Get values from a checkpoint asynchronously.
*
* @param checkpointId The ID of the checkpoint to load
* @return CompletableFuture with the channel values from the checkpoint, or empty if not found
*/
CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId);
/**
* List all checkpoints for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture with list of checkpoint IDs
*/
CompletableFuture<List<String>> listAsync(String threadId);
/**
* Get the latest checkpoint for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture with the ID of the latest checkpoint, or empty if none exists
*/
CompletableFuture<Optional<String>> latestAsync(String threadId);
/**
* Delete a checkpoint asynchronously.
*
* @param checkpointId The ID of the checkpoint to delete
* @return CompletableFuture completed when deletion is done
*/
CompletableFuture<Void> deleteAsync(String checkpointId);
/**
* Clear all checkpoints for a thread asynchronously.
*
* @param threadId The ID of the thread
* @return CompletableFuture completed when clearing is done
*/
CompletableFuture<Void> clearAsync(String threadId);
}
@@ -0,0 +1,57 @@
package com.langgraph.checkpoint.base;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Interface for saving and loading checkpoints.
*/
public interface BaseCheckpointSaver {
/**
* Create a new checkpoint.
*
* @param threadId The ID of the thread to checkpoint
* @param channelValues The values of the channels to checkpoint
* @return The ID of the new checkpoint
*/
String checkpoint(String threadId, Map<String, Object> channelValues);
/**
* Get values from a checkpoint.
*
* @param checkpointId The ID of the checkpoint to load
* @return The channel values from the checkpoint, or empty if not found
*/
Optional<Map<String, Object>> getValues(String checkpointId);
/**
* List all checkpoints for a thread.
*
* @param threadId The ID of the thread
* @return List of checkpoint IDs
*/
List<String> list(String threadId);
/**
* Get the latest checkpoint for a thread.
*
* @param threadId The ID of the thread
* @return The ID of the latest checkpoint, or empty if none exists
*/
Optional<String> latest(String threadId);
/**
* Delete a checkpoint.
*
* @param checkpointId The ID of the checkpoint to delete
*/
void delete(String checkpointId);
/**
* Clear all checkpoints for a thread.
*
* @param threadId The ID of the thread
*/
void clear(String threadId);
}
@@ -0,0 +1,81 @@
package com.langgraph.checkpoint.base;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.UUID;
/**
* Utility class for generating deterministic IDs.
*/
public final class ID {
private ID() {
// Prevent instantiation
}
/**
* Generate a deterministic UUID based on a namespace and name.
*
* @param namespace The namespace for the ID
* @param name The name within the namespace
* @return A UUID
*/
public static UUID uuid(String namespace, String name) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(namespace.getBytes(StandardCharsets.UTF_8));
md.update(name.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
// Set the version (4) and variant (RFC4122) bits
digest[6] = (byte) ((digest[6] & 0x0F) | 0x40);
digest[8] = (byte) ((digest[8] & 0x3F) | 0x80);
long msb = 0;
long lsb = 0;
for (int i = 0; i < 8; i++) {
msb = (msb << 8) | (digest[i] & 0xff);
}
for (int i = 8; i < 16; i++) {
lsb = (lsb << 8) | (digest[i] & 0xff);
}
return new UUID(msb, lsb);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 algorithm not available", e);
}
}
/**
* Generate a checkpoint ID.
*
* @param threadId The thread ID
* @return A checkpoint ID
*/
public static String checkpointId(String threadId) {
return uuid("checkpoint", threadId + "/" + System.currentTimeMillis()).toString();
}
/**
* Generate a URL-safe base64 encoded ID.
*
* @param namespace The namespace for the ID
* @param name The name within the namespace
* @return A URL-safe base64-encoded ID
*/
public static String urlSafeId(String namespace, String name) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(namespace.getBytes(StandardCharsets.UTF_8));
md.update(name.getBytes(StandardCharsets.UTF_8));
byte[] digest = md.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}
}
@@ -0,0 +1,79 @@
package com.langgraph.checkpoint.base.memory;
import com.langgraph.checkpoint.base.AsyncBaseCheckpointSaver;
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Asynchronous in-memory implementation of a checkpoint saver.
* This is a thin wrapper around the synchronous implementation that
* executes operations asynchronously.
*/
public class AsyncMemoryCheckpointSaver implements AsyncBaseCheckpointSaver {
private final BaseCheckpointSaver synchronousSaver;
/**
* Create an async memory checkpoint saver.
*/
public AsyncMemoryCheckpointSaver() {
this.synchronousSaver = new MemoryCheckpointSaver();
}
/**
* Create an async memory checkpoint saver with an existing synchronous saver.
*
* @param synchronousSaver The synchronous checkpoint saver to wrap
*/
public AsyncMemoryCheckpointSaver(BaseCheckpointSaver synchronousSaver) {
this.synchronousSaver = synchronousSaver;
}
@Override
public CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.checkpoint(threadId, channelValues));
}
@Override
public CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.getValues(checkpointId));
}
@Override
public CompletableFuture<List<String>> listAsync(String threadId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.list(threadId));
}
@Override
public CompletableFuture<Optional<String>> latestAsync(String threadId) {
return CompletableFuture.supplyAsync(() ->
synchronousSaver.latest(threadId));
}
@Override
public CompletableFuture<Void> deleteAsync(String checkpointId) {
return CompletableFuture.runAsync(() ->
synchronousSaver.delete(checkpointId));
}
@Override
public CompletableFuture<Void> clearAsync(String threadId) {
return CompletableFuture.runAsync(() ->
synchronousSaver.clear(threadId));
}
/**
* Get the underlying synchronous checkpoint saver.
*
* @return The synchronous checkpoint saver
*/
public BaseCheckpointSaver getSynchronousSaver() {
return synchronousSaver;
}
}
@@ -0,0 +1,77 @@
package com.langgraph.checkpoint.base.memory;
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
import com.langgraph.checkpoint.base.ID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* In-memory implementation of a checkpoint saver.
*/
public class MemoryCheckpointSaver implements BaseCheckpointSaver {
private final Map<String, Map<String, Object>> checkpoints = new ConcurrentHashMap<>();
private final Map<String, List<String>> threadCheckpoints = new ConcurrentHashMap<>();
@Override
public String checkpoint(String threadId, Map<String, Object> channelValues) {
String checkpointId = ID.checkpointId(threadId);
// Store the checkpoint
checkpoints.put(checkpointId, new HashMap<>(channelValues));
// Add to thread's checkpoints
threadCheckpoints.computeIfAbsent(threadId, k ->
Collections.synchronizedList(new ArrayList<>())).add(checkpointId);
return checkpointId;
}
@Override
public Optional<Map<String, Object>> getValues(String checkpointId) {
Map<String, Object> values = checkpoints.get(checkpointId);
return Optional.ofNullable(values).map(HashMap::new);
}
@Override
public List<String> list(String threadId) {
List<String> result = threadCheckpoints.get(threadId);
return result != null ? new ArrayList<>(result) : Collections.emptyList();
}
@Override
public Optional<String> latest(String threadId) {
List<String> checkpoints = threadCheckpoints.get(threadId);
if (checkpoints == null || checkpoints.isEmpty()) {
return Optional.empty();
}
return Optional.of(checkpoints.get(checkpoints.size() - 1));
}
@Override
public void delete(String checkpointId) {
// Remove the checkpoint
Map<String, Object> removed = checkpoints.remove(checkpointId);
if (removed != null) {
// Find and remove from thread's checkpoints
for (List<String> checkpointsList : threadCheckpoints.values()) {
checkpointsList.remove(checkpointId);
}
}
}
@Override
public void clear(String threadId) {
List<String> checkpointIds = threadCheckpoints.remove(threadId);
if (checkpointIds != null) {
// Remove all checkpoints for this thread
for (String checkpointId : checkpointIds) {
checkpoints.remove(checkpointId);
}
}
}
}
@@ -0,0 +1,548 @@
package com.langgraph.checkpoint.serde;
import org.msgpack.core.MessageBufferPacker;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.core.MessageFormat;
import java.io.IOException;
import java.lang.reflect.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* MessagePack-based serializer that uses reflection to handle arbitrary Java objects.
* Supports primitive types, collections, maps, records, and custom objects.
*/
public class MsgPackSerializer implements ReflectionSerializer {
private final Map<Class<?>, TypeSerializer<?>> serializers = new ConcurrentHashMap<>();
private final Map<Class<?>, TypeDeserializer<?>> deserializers = new ConcurrentHashMap<>();
private final Map<Class<?>, RecordInfo> recordInfoCache = new ConcurrentHashMap<>();
/**
* Record component information cache to avoid repeated reflection.
*/
private static class RecordInfo {
final RecordComponent[] components;
final Constructor<?> constructor;
RecordInfo(RecordComponent[] components, Constructor<?> constructor) {
this.components = components;
this.constructor = constructor;
}
}
/**
* Register built-in serializers for common types.
*/
public MsgPackSerializer() {
registerBuiltinTypes();
}
/**
* Register built-in serializers for common types.
*/
private void registerBuiltinTypes() {
// UUID serializer
registerSerializer(UUID.class, (uuid) -> uuid.toString());
registerDeserializer(UUID.class, (str) -> UUID.fromString((String) str));
// Date serializer
registerSerializer(java.util.Date.class, (date) -> date.getTime());
registerDeserializer(java.util.Date.class, (millis) -> new Date((Long) millis));
// Java 8 Date/Time API
registerSerializer(Instant.class, (instant) -> instant.toString());
registerDeserializer(Instant.class, (str) -> Instant.parse((String) str));
registerSerializer(LocalDate.class, (date) -> date.toString());
registerDeserializer(LocalDate.class, (str) -> LocalDate.parse((String) str));
registerSerializer(LocalTime.class, (time) -> time.toString());
registerDeserializer(LocalTime.class, (str) -> LocalTime.parse((String) str));
registerSerializer(LocalDateTime.class, (dateTime) -> dateTime.toString());
registerDeserializer(LocalDateTime.class, (str) -> LocalDateTime.parse((String) str));
// Add more built-in serializers as needed
}
@Override
public <T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer) {
serializers.put(type, serializer);
}
@Override
public <T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer) {
deserializers.put(type, deserializer);
}
@Override
public byte[] serialize(Object obj) {
try {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
serializeObject(obj, packer);
return packer.toByteArray();
} catch (IOException e) {
throw new SerializationException("Failed to serialize object", e);
}
}
@Override
public Object deserialize(byte[] data) {
try {
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data);
return deserializeObject(unpacker);
} catch (IOException e) {
throw new SerializationException("Failed to deserialize object", e);
}
}
/**
* Serialize an object to the MessagePack packer.
*
* @param obj Object to serialize
* @param packer MessagePack packer
* @throws IOException If packing fails
*/
@SuppressWarnings("unchecked")
private void serializeObject(Object obj, MessageBufferPacker packer) throws IOException {
if (obj == null) {
packer.packNil();
return;
}
Class<?> type = obj.getClass();
// Check for registered serializer
if (serializers.containsKey(type)) {
TypeSerializer<Object> serializer = (TypeSerializer<Object>) serializers.get(type);
Object serialized = serializer.toSerializable(obj);
// Pack as a special type
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(type.getName());
packer.packString("value");
serializeObject(serialized, packer);
return;
}
// Handle primitive types and common objects directly
if (obj instanceof String) {
packer.packString((String) obj);
} else if (obj instanceof Integer) {
packer.packInt((Integer) obj);
} else if (obj instanceof Long) {
packer.packLong((Long) obj);
} else if (obj instanceof Double) {
packer.packDouble((Double) obj);
} else if (obj instanceof Float) {
packer.packFloat((Float) obj);
} else if (obj instanceof Boolean) {
packer.packBoolean((Boolean) obj);
} else if (obj instanceof byte[]) {
packer.packBinaryHeader(((byte[]) obj).length);
packer.writePayload((byte[]) obj);
} else if (obj instanceof List) {
List<?> list = (List<?>) obj;
packer.packArrayHeader(list.size());
for (Object item : list) {
serializeObject(item, packer);
}
} else if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
packer.packMapHeader(map.size());
for (Map.Entry<?, ?> entry : map.entrySet()) {
serializeObject(entry.getKey(), packer);
serializeObject(entry.getValue(), packer);
}
} else if (obj instanceof Enum<?>) {
// Handle enums by name
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(type.getName());
packer.packString("value");
packer.packString(((Enum<?>) obj).name());
} else if (type.isRecord()) {
// Handle Record types
serializeRecord(obj, packer);
} else {
// Handle custom objects with reflection
serializeCustomObject(obj, packer);
}
}
/**
* Serialize a Record object.
*
* @param record The record to serialize
* @param packer The MessagePack packer
* @throws IOException If packing fails
*/
private void serializeRecord(Object record, MessageBufferPacker packer) throws IOException {
Class<?> recordClass = record.getClass();
// Pack as a special type with fields
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(recordClass.getName());
packer.packString("fields");
RecordComponent[] components = recordClass.getRecordComponents();
packer.packMapHeader(components.length);
for (RecordComponent component : components) {
packer.packString(component.getName());
try {
Method accessor = component.getAccessor();
Object value = accessor.invoke(record);
serializeObject(value, packer);
} catch (ReflectiveOperationException e) {
throw new SerializationException("Failed to access record component: " + component.getName(), e);
}
}
}
/**
* Serialize a custom object using reflection.
*
* @param obj The object to serialize
* @param packer The MessagePack packer
* @throws IOException If packing fails
*/
private void serializeCustomObject(Object obj, MessageBufferPacker packer) throws IOException {
Class<?> objClass = obj.getClass();
// Pack as a special type with fields
packer.packMapHeader(2);
packer.packString("__type__");
packer.packString(objClass.getName());
packer.packString("fields");
// Get all fields including inherited ones
List<Field> fields = getAllFields(objClass);
// Filter out transient fields
List<Field> serializableFields = fields.stream()
.filter(field -> !Modifier.isTransient(field.getModifiers()) &&
!Modifier.isStatic(field.getModifiers()))
.toList();
packer.packMapHeader(serializableFields.size());
for (Field field : serializableFields) {
packer.packString(field.getName());
try {
field.setAccessible(true);
Object value = field.get(obj);
serializeObject(value, packer);
} catch (IllegalAccessException e) {
throw new SerializationException("Failed to access field: " + field.getName(), e);
}
}
}
/**
* Get all fields for a class including inherited fields.
*
* @param clazz The class to get fields for
* @return List of all fields
*/
private List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return fields;
}
/**
* Deserialize an object from the MessagePack unpacker.
*
* @param unpacker MessagePack unpacker
* @return Deserialized object
* @throws IOException If unpacking fails
*/
@SuppressWarnings("unchecked")
private Object deserializeObject(MessageUnpacker unpacker) throws IOException {
if (!unpacker.hasNext()) {
throw new SerializationException("Unexpected end of data");
}
if (unpacker.tryUnpackNil()) {
return null;
}
MessageFormat format = unpacker.getNextFormat();
if (format == MessageFormat.STR8 ||
format == MessageFormat.STR16 ||
format == MessageFormat.STR32 ||
format == MessageFormat.FIXSTR) {
return unpacker.unpackString();
} else if (format == MessageFormat.INT8 ||
format == MessageFormat.INT16 ||
format == MessageFormat.INT32 ||
format == MessageFormat.INT64 ||
format == MessageFormat.UINT8 ||
format == MessageFormat.UINT16 ||
format == MessageFormat.UINT32 ||
format == MessageFormat.UINT64 ||
format == MessageFormat.POSFIXINT ||
format == MessageFormat.NEGFIXINT) {
if (format == MessageFormat.INT64 || format == MessageFormat.UINT64) {
return unpacker.unpackLong();
} else {
try {
return unpacker.unpackInt();
} catch (Exception e) {
// Fallback to long if int unpacking fails
return unpacker.unpackLong();
}
}
} else if (format == MessageFormat.FLOAT32 ||
format == MessageFormat.FLOAT64) {
return unpacker.unpackDouble();
} else if (format == MessageFormat.BOOLEAN) {
return unpacker.unpackBoolean();
} else if (format == MessageFormat.BIN8 ||
format == MessageFormat.BIN16 ||
format == MessageFormat.BIN32) {
int binaryLength = unpacker.unpackBinaryHeader();
byte[] binary = new byte[binaryLength];
unpacker.readPayload(binary);
return binary;
} else if (format == MessageFormat.ARRAY16 ||
format == MessageFormat.ARRAY32 ||
format == MessageFormat.FIXARRAY) {
int arraySize = unpacker.unpackArrayHeader();
List<Object> list = new ArrayList<>(arraySize);
for (int i = 0; i < arraySize; i++) {
list.add(deserializeObject(unpacker));
}
return list;
} else if (format == MessageFormat.MAP16 ||
format == MessageFormat.MAP32 ||
format == MessageFormat.FIXMAP) {
int mapSize = unpacker.unpackMapHeader();
// Handle empty map
if (mapSize == 0) {
return new HashMap<>();
}
// Check for special type marker
Object firstKey = deserializeObject(unpacker);
if (mapSize == 2 && firstKey instanceof String && "__type__".equals(firstKey)) {
String typeName = (String) deserializeObject(unpacker);
// Get the second key
Object secondKey = deserializeObject(unpacker);
if (secondKey instanceof String) {
String secondKeyStr = (String) secondKey;
try {
Class<?> type = Class.forName(typeName);
// Check for registered deserializer
if ("value".equals(secondKeyStr) && deserializers.containsKey(type)) {
Object serialized = deserializeObject(unpacker);
TypeDeserializer<Object> deserializer =
(TypeDeserializer<Object>) deserializers.get(type);
return deserializer.fromSerialized(serialized);
}
// Handle enums
if ("value".equals(secondKeyStr) && type.isEnum()) {
String enumValue = (String) deserializeObject(unpacker);
return Enum.valueOf((Class<Enum>) type, enumValue);
}
// Handle records
if ("fields".equals(secondKeyStr) && type.isRecord()) {
return deserializeRecord(type, unpacker);
}
// Handle custom objects
if ("fields".equals(secondKeyStr)) {
return deserializeCustomObject(type, unpacker);
}
} catch (ClassNotFoundException e) {
// If class not found, fall back to regular map deserialization
} catch (ReflectiveOperationException e) {
throw new SerializationException("Failed to deserialize object of type " + typeName, e);
}
// If special type handling failed, read the value to keep unpacker consistent
Object secondValue = deserializeObject(unpacker);
// Create a fallback map with the special type info
Map<Object, Object> fallbackMap = new HashMap<>();
fallbackMap.put(firstKey, typeName);
fallbackMap.put(secondKey, secondValue);
return fallbackMap;
}
// If the second key wasn't a string as expected, we need to handle it as a regular map
Object firstValue = deserializeObject(unpacker);
// Create a map with the first key-value pair
Map<Object, Object> map = new HashMap<>(mapSize);
map.put(firstKey, firstValue);
// Read the remaining entries
for (int i = 1; i < mapSize; i++) {
Object key = deserializeObject(unpacker);
Object value = deserializeObject(unpacker);
map.put(key, value);
}
return map;
} else {
// Regular map - we already read the first key
Map<Object, Object> map = new HashMap<>(mapSize);
// Read the first value
Object firstValue = deserializeObject(unpacker);
map.put(firstKey, firstValue);
// Read the remaining entries
for (int i = 1; i < mapSize; i++) {
Object key = deserializeObject(unpacker);
Object value = deserializeObject(unpacker);
map.put(key, value);
}
return map;
}
}
// Default case
throw new SerializationException("Unsupported MessagePack format: " + format);
}
/**
* Deserialize a record.
*
* @param recordClass The record class
* @param unpacker The unpacker containing the fields map
* @return The deserialized record
* @throws IOException If unpacking fails
* @throws ReflectiveOperationException If reflection operations fail
*/
private Object deserializeRecord(Class<?> recordClass, MessageUnpacker unpacker)
throws IOException, ReflectiveOperationException {
// Get record info from cache or create it
RecordInfo recordInfo = recordInfoCache.computeIfAbsent(recordClass, cls -> {
try {
RecordComponent[] components = cls.getRecordComponents();
Class<?>[] paramTypes = Arrays.stream(components)
.map(RecordComponent::getType)
.toArray(Class<?>[]::new);
Constructor<?> constructor = cls.getDeclaredConstructor(paramTypes);
constructor.setAccessible(true);
return new RecordInfo(components, constructor);
} catch (NoSuchMethodException e) {
throw new SerializationException("Failed to get constructor for record: " + cls.getName(), e);
}
});
// Read the fields map
int fieldCount = unpacker.unpackMapHeader();
Map<String, Object> fieldValues = new HashMap<>(fieldCount);
for (int i = 0; i < fieldCount; i++) {
String fieldName = (String) deserializeObject(unpacker);
Object fieldValue = deserializeObject(unpacker);
fieldValues.put(fieldName, fieldValue);
}
// Prepare constructor arguments in the correct order
Object[] constructorArgs = new Object[recordInfo.components.length];
for (int i = 0; i < recordInfo.components.length; i++) {
RecordComponent component = recordInfo.components[i];
Object value = fieldValues.get(component.getName());
constructorArgs[i] = value;
}
// Create the record instance
return recordInfo.constructor.newInstance(constructorArgs);
}
/**
* Deserialize a custom object.
*
* @param objectClass The object class
* @param unpacker The unpacker containing the fields map
* @return The deserialized object
* @throws IOException If unpacking fails
* @throws ReflectiveOperationException If reflection operations fail
*/
private Object deserializeCustomObject(Class<?> objectClass, MessageUnpacker unpacker)
throws IOException, ReflectiveOperationException {
// Create instance using default constructor
Constructor<?> constructor;
try {
constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new SerializationException(
"Class " + objectClass.getName() + " must have a no-arg constructor for deserialization", e);
}
Object instance = constructor.newInstance();
// Read the fields map
int fieldCount = unpacker.unpackMapHeader();
for (int i = 0; i < fieldCount; i++) {
String fieldName = (String) deserializeObject(unpacker);
Object fieldValue = deserializeObject(unpacker);
try {
// Find the field (including in superclasses)
Field field = findField(objectClass, fieldName);
if (field != null) {
field.setAccessible(true);
field.set(instance, fieldValue);
}
} catch (NoSuchFieldException e) {
// Skip fields that don't exist in the current class version
}
}
return instance;
}
/**
* Find a field in a class or its superclasses.
*
* @param clazz The class to search
* @param fieldName The field name to find
* @return The found field
* @throws NoSuchFieldException If the field is not found
*/
private Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
Class<?> currentClass = clazz;
while (currentClass != null) {
try {
return currentClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
currentClass = currentClass.getSuperclass();
}
}
throw new NoSuchFieldException("Field not found: " + fieldName);
}
}
@@ -0,0 +1,24 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for a serializer that uses reflection to handle arbitrary Java objects.
*/
public interface ReflectionSerializer extends Serializer<Object> {
/**
* Register a custom serializer for a specific type.
*
* @param type Type to register
* @param serializer Custom serializer for the type
* @param <T> Type to register
*/
<T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer);
/**
* Register a custom deserializer for a specific type.
*
* @param type Type to register
* @param deserializer Custom deserializer for the type
* @param <T> Type to register
*/
<T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer);
}
@@ -0,0 +1,25 @@
package com.langgraph.checkpoint.serde;
/**
* Exception thrown during serialization/deserialization.
*/
public class SerializationException extends RuntimeException {
/**
* Create a new serialization exception with a message.
*
* @param message Error message
*/
public SerializationException(String message) {
super(message);
}
/**
* Create a new serialization exception with a message and cause.
*
* @param message Error message
* @param cause Underlying cause
*/
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,24 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for serializing and deserializing objects.
*
* @param <T> Type of object to serialize/deserialize
*/
public interface Serializer<T> {
/**
* Serialize an object to bytes.
*
* @param obj The object to serialize
* @return Serialized bytes
*/
byte[] serialize(T obj);
/**
* Deserialize bytes to an object.
*
* @param data The bytes to deserialize
* @return Deserialized object
*/
T deserialize(byte[] data);
}
@@ -0,0 +1,17 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for deserializing a specific type from MessagePack.
*
* @param <T> Type to deserialize
*/
@FunctionalInterface
public interface TypeDeserializer<T> {
/**
* Convert from serialized representation to object.
*
* @param serialized Serialized representation
* @return Deserialized object
*/
T fromSerialized(Object serialized);
}
@@ -0,0 +1,17 @@
package com.langgraph.checkpoint.serde;
/**
* Interface for serializing a specific type to a format that can be included in MessagePack.
*
* @param <T> Type to serialize
*/
@FunctionalInterface
public interface TypeSerializer<T> {
/**
* Convert object to a serializable representation.
*
* @param obj Object to convert
* @return Serializable representation (must be compatible with MessagePack)
*/
Object toSerializable(T obj);
}
@@ -0,0 +1,59 @@
package com.langgraph.checkpoint.base;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
public class IDTest {
@Test
public void testUuidDeterministic() {
// Same inputs should produce same UUIDs
UUID uuid1 = ID.uuid("test", "value");
UUID uuid2 = ID.uuid("test", "value");
assertThat(uuid1).isEqualTo(uuid2);
}
@Test
public void testUuidDifferentNamespace() {
// Different namespaces should produce different UUIDs
UUID uuid1 = ID.uuid("namespace1", "value");
UUID uuid2 = ID.uuid("namespace2", "value");
assertThat(uuid1).isNotEqualTo(uuid2);
}
@Test
public void testUuidDifferentName() {
// Different names should produce different UUIDs
UUID uuid1 = ID.uuid("test", "value1");
UUID uuid2 = ID.uuid("test", "value2");
assertThat(uuid1).isNotEqualTo(uuid2);
}
@Test
public void testCheckpointId() {
// Checkpoint IDs should be valid UUIDs
String id = ID.checkpointId("thread-123");
// Should be a valid UUID string
UUID uuid = UUID.fromString(id);
assertThat(uuid).isNotNull();
}
@Test
public void testUrlSafeId() {
// URL-safe IDs should be deterministic
String id1 = ID.urlSafeId("test", "value");
String id2 = ID.urlSafeId("test", "value");
assertThat(id1).isEqualTo(id2);
// Should not contain padding characters or unsafe URL characters
assertThat(id1).doesNotContain("=", "+", "/");
}
}
@@ -0,0 +1,155 @@
package com.langgraph.checkpoint.base.memory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
public class AsyncMemoryCheckpointSaverTest {
private AsyncMemoryCheckpointSaver saver;
@BeforeEach
public void setUp() {
saver = new AsyncMemoryCheckpointSaver();
}
@Test
public void testCheckpointAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint asynchronously
CompletableFuture<String> future = saver.checkpointAsync(threadId, values);
// Wait for completion
String checkpointId = future.get();
// Verify checkpoint ID format (should be a UUID)
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
// Verify thread has a checkpoint
CompletableFuture<List<String>> listFuture = saver.listAsync(threadId);
List<String> checkpoints = listFuture.get();
assertThat(checkpoints).hasSize(1);
assertThat(checkpoints).contains(checkpointId);
}
@Test
public void testGetValuesAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpointAsync(threadId, values).get();
// Get values asynchronously
CompletableFuture<Optional<Map<String, Object>>> future = saver.getValuesAsync(checkpointId);
Optional<Map<String, Object>> retrievedValues = future.get();
// Verify values
assertThat(retrievedValues).isPresent();
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
assertThat(retrievedValues.get()).containsEntry("key2", 42);
}
@Test
public void testListAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Initially empty
CompletableFuture<List<String>> initialFuture = saver.listAsync(threadId);
List<String> initial = initialFuture.get();
assertThat(initial).isEmpty();
// Create multiple checkpoints
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
// List checkpoints asynchronously
CompletableFuture<List<String>> future = saver.listAsync(threadId);
List<String> checkpoints = future.get();
// Verify order and content
assertThat(checkpoints).hasSize(3);
assertThat(checkpoints).containsExactly(id1, id2, id3);
}
@Test
public void testLatestAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Initially empty
CompletableFuture<Optional<String>> initialFuture = saver.latestAsync(threadId);
Optional<String> initial = initialFuture.get();
assertThat(initial).isEmpty();
// Create multiple checkpoints
saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
// Get latest asynchronously
CompletableFuture<Optional<String>> future = saver.latestAsync(threadId);
Optional<String> latest = future.get();
// Verify latest
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(id3);
}
@Test
public void testDeleteAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Create checkpoint
String checkpointId = saver.checkpointAsync(threadId, Map.of("key", "value")).get();
// Verify checkpoint exists
assertThat(saver.getValuesAsync(checkpointId).get()).isPresent();
// Delete checkpoint asynchronously
CompletableFuture<Void> future = saver.deleteAsync(checkpointId);
future.get(); // Wait for completion
// Verify checkpoint is deleted
assertThat(saver.getValuesAsync(checkpointId).get()).isEmpty();
}
@Test
public void testClearAsync() throws ExecutionException, InterruptedException {
// Create test data
String threadId = "test-thread";
// Create multiple checkpoints
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
// Verify checkpoints exist
assertThat(saver.listAsync(threadId).get()).hasSize(2);
// Clear thread asynchronously
CompletableFuture<Void> future = saver.clearAsync(threadId);
future.get(); // Wait for completion
// Verify checkpoints are deleted
assertThat(saver.listAsync(threadId).get()).isEmpty();
}
}
@@ -0,0 +1,161 @@
package com.langgraph.checkpoint.base.memory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
public class MemoryCheckpointSaverTest {
private MemoryCheckpointSaver saver;
@BeforeEach
public void setUp() {
saver = new MemoryCheckpointSaver();
}
@Test
public void testCheckpoint() {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, values);
// Verify checkpoint ID format (should be a UUID)
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
// Verify thread has a checkpoint
List<String> checkpoints = saver.list(threadId);
assertThat(checkpoints).hasSize(1);
assertThat(checkpoints).contains(checkpointId);
// Verify latest checkpoint
Optional<String> latest = saver.latest(threadId);
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(checkpointId);
}
@Test
public void testGetValues() {
// Create test data
String threadId = "test-thread";
Map<String, Object> values = new HashMap<>();
values.put("key1", "value1");
values.put("key2", 42);
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, values);
// Get values
Optional<Map<String, Object>> retrievedValues = saver.getValues(checkpointId);
// Verify values
assertThat(retrievedValues).isPresent();
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
assertThat(retrievedValues.get()).containsEntry("key2", 42);
// Verify non-existent checkpoint
Optional<Map<String, Object>> nonExistent = saver.getValues("non-existent");
assertThat(nonExistent).isEmpty();
}
@Test
public void testList() {
// Create test data
String threadId = "test-thread";
// Initially empty
List<String> initial = saver.list(threadId);
assertThat(initial).isEmpty();
// Create multiple checkpoints
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
// List checkpoints
List<String> checkpoints = saver.list(threadId);
// Verify order and content
assertThat(checkpoints).hasSize(3);
assertThat(checkpoints).containsExactly(id1, id2, id3);
// Different thread should have no checkpoints
List<String> otherThread = saver.list("other-thread");
assertThat(otherThread).isEmpty();
}
@Test
public void testLatest() {
// Create test data
String threadId = "test-thread";
// Initially empty
Optional<String> initial = saver.latest(threadId);
assertThat(initial).isEmpty();
// Create multiple checkpoints
saver.checkpoint(threadId, Map.of("key", "value1"));
saver.checkpoint(threadId, Map.of("key", "value2"));
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
// Get latest
Optional<String> latest = saver.latest(threadId);
// Verify latest
assertThat(latest).isPresent();
assertThat(latest.get()).isEqualTo(id3);
}
@Test
public void testDelete() {
// Create test data
String threadId = "test-thread";
// Create checkpoint
String checkpointId = saver.checkpoint(threadId, Map.of("key", "value"));
// Verify checkpoint exists
assertThat(saver.getValues(checkpointId)).isPresent();
assertThat(saver.list(threadId)).contains(checkpointId);
// Delete checkpoint
saver.delete(checkpointId);
// Verify checkpoint is deleted
assertThat(saver.getValues(checkpointId)).isEmpty();
assertThat(saver.list(threadId)).doesNotContain(checkpointId);
}
@Test
public void testClear() {
// Create test data
String threadId = "test-thread";
// Create multiple checkpoints
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
// Verify checkpoints exist
assertThat(saver.list(threadId)).hasSize(2);
assertThat(saver.getValues(id1)).isPresent();
assertThat(saver.getValues(id2)).isPresent();
// Clear thread
saver.clear(threadId);
// Verify checkpoints are deleted
assertThat(saver.list(threadId)).isEmpty();
assertThat(saver.getValues(id1)).isEmpty();
assertThat(saver.getValues(id2)).isEmpty();
}
}
@@ -0,0 +1,383 @@
package com.langgraph.checkpoint.serde;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
public class MsgPackSerializerTest {
private MsgPackSerializer serializer;
@BeforeEach
public void setUp() {
serializer = new MsgPackSerializer();
}
@Test
public void testSerializeDeserializePrimitives() {
// Test with various primitive types
assertRoundTrip("Test string");
assertRoundTrip(123);
assertRoundTrip(123456789L);
assertRoundTrip(123.45);
assertRoundTrip(123.45f);
assertRoundTrip(true);
assertRoundTrip(false);
assertRoundTrip(null);
}
@Test
public void testSerializeDeserializeArrays() {
// Test with arrays and collections
assertRoundTrip(new byte[] {1, 2, 3, 4, 5});
assertRoundTrip(Arrays.asList("one", "two", "three"));
assertRoundTrip(Arrays.asList(1, 2, 3, 4, 5));
}
@Test
public void testSerializeDeserializeMaps() {
// Test with maps
Map<String, Object> map = new HashMap<>();
map.put("string", "value");
map.put("int", 123);
map.put("boolean", true);
assertRoundTrip(map);
}
@Test
public void testSerializeDeserializeNestedStructures() {
// Test with nested structures
Map<String, Object> nested = new HashMap<>();
nested.put("list", Arrays.asList(1, 2, 3));
nested.put("map", Map.of("key", "value"));
assertRoundTrip(nested);
}
@Test
public void testSerializeDeserializeEnums() {
// Test with enums
assertRoundTrip(TestEnum.VALUE1);
assertRoundTrip(TestEnum.VALUE2);
assertRoundTrip(TestEnum.VALUE3);
}
@Test
public void testSerializeDeserializeRecord() {
// Test with a record
TestRecord record = new TestRecord("test", 123, Arrays.asList("a", "b", "c"));
// Serialize and deserialize
byte[] serialized = serializer.serialize(record);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(TestRecord.class);
TestRecord deserializedRecord = (TestRecord) deserialized;
assertThat(deserializedRecord.name()).isEqualTo("test");
assertThat(deserializedRecord.value()).isEqualTo(123);
assertThat(deserializedRecord.tags()).containsExactly("a", "b", "c");
}
@Test
public void testSerializeDeserializeNestedRecord() {
// Test with a nested record
NestedTestRecord record = new NestedTestRecord(
"parent",
new TestRecord("child", 456, Arrays.asList("x", "y", "z"))
);
// Serialize and deserialize
byte[] serialized = serializer.serialize(record);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(NestedTestRecord.class);
NestedTestRecord deserializedRecord = (NestedTestRecord) deserialized;
assertThat(deserializedRecord.name()).isEqualTo("parent");
assertThat(deserializedRecord.child()).isInstanceOf(TestRecord.class);
assertThat(deserializedRecord.child().name()).isEqualTo("child");
assertThat(deserializedRecord.child().value()).isEqualTo(456);
assertThat(deserializedRecord.child().tags()).containsExactly("x", "y", "z");
}
@Test
public void testSerializeDeserializeCustomObject() {
// Test with a custom object
TestObject obj = new TestObject();
obj.setName("test");
obj.setValue(123);
obj.setActive(true);
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(TestObject.class);
TestObject deserializedObj = (TestObject) deserialized;
assertThat(deserializedObj.getName()).isEqualTo("test");
assertThat(deserializedObj.getValue()).isEqualTo(123);
assertThat(deserializedObj.isActive()).isTrue();
}
@Test
public void testSerializeDeserializeInheritance() {
// Test with inheritance
ChildTestObject obj = new ChildTestObject();
obj.setName("parent");
obj.setValue(123);
obj.setActive(true);
obj.setChildProperty("child");
obj.setChildValue(456);
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(ChildTestObject.class);
ChildTestObject deserializedObj = (ChildTestObject) deserialized;
assertThat(deserializedObj.getName()).isEqualTo("parent");
assertThat(deserializedObj.getValue()).isEqualTo(123);
assertThat(deserializedObj.isActive()).isTrue();
assertThat(deserializedObj.getChildProperty()).isEqualTo("child");
assertThat(deserializedObj.getChildValue()).isEqualTo(456);
}
@Test
public void testSerializeDeserializeWithCustomSerializer() {
// Register custom UUID serializer (although built-in one exists)
serializer.registerSerializer(UUID.class, (uuid) -> uuid.toString().replace("-", ""));
serializer.registerDeserializer(UUID.class, (str) -> {
String uuidStr = (String) str;
// Insert hyphens for standard UUID format
uuidStr = uuidStr.replaceFirst(
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
"$1-$2-$3-$4-$5");
return UUID.fromString(uuidStr);
});
// Test with UUID
UUID uuid = UUID.randomUUID();
// Serialize and deserialize
byte[] serialized = serializer.serialize(uuid);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(UUID.class);
assertThat(deserialized).isEqualTo(uuid);
}
@Test
public void testSerializeDeserializeDateTypes() {
// Test with Date
Date date = new Date();
// Serialize and deserialize
byte[] serialized = serializer.serialize(date);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(Date.class);
assertThat(deserialized).isEqualTo(date);
// Test with Java 8 Date/Time types
Instant instant = Instant.now();
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
assertRoundTrip(instant);
assertRoundTrip(localDate);
assertRoundTrip(localTime);
assertRoundTrip(localDateTime);
}
@Test
public void testTransientFields() {
// Test with transient fields
ObjectWithTransient obj = new ObjectWithTransient();
obj.setPersistent("saved");
obj.setTransientField("not-saved");
// Serialize and deserialize
byte[] serialized = serializer.serialize(obj);
Object deserialized = serializer.deserialize(serialized);
// Verify
assertThat(deserialized).isInstanceOf(ObjectWithTransient.class);
ObjectWithTransient deserializedObj = (ObjectWithTransient) deserialized;
assertThat(deserializedObj.getPersistent()).isEqualTo("saved");
assertThat(deserializedObj.getTransientField()).isNull(); // Should be null after deserialization
}
/**
* Helper method to assert that an object survives a round trip through serialization.
*
* @param obj Object to test
*/
private void assertRoundTrip(Object obj) {
try {
// Serialize
byte[] serialized = serializer.serialize(obj);
// Deserialize
Object deserialized = serializer.deserialize(serialized);
// Verify
if (obj instanceof byte[]) {
// Arrays need special comparison
assertThat(deserialized).isInstanceOf(byte[].class);
assertThat((byte[]) deserialized).isEqualTo((byte[]) obj);
} else if (obj instanceof Number) {
// For any number type, compare by value instead of exact type
if (deserialized instanceof Number) {
double expected = ((Number) obj).doubleValue();
double actual = ((Number) deserialized).doubleValue();
assertThat(actual).isCloseTo(expected, within(0.0001));
} else {
throw new AssertionError("Expected Number, got " +
(deserialized != null ? deserialized.getClass().getName() : "null"));
}
} else {
// Special handling for lists
if (obj instanceof List && deserialized instanceof List) {
List<?> originalList = (List<?>) obj;
List<?> deserializedList = (List<?>) deserialized;
assertThat(deserializedList).hasSameSizeAs(originalList);
// Check each element
for (int i = 0; i < originalList.size(); i++) {
Object originalItem = originalList.get(i);
Object deserializedItem = deserializedList.get(i);
if (originalItem instanceof Number && deserializedItem instanceof Number) {
// Compare numbers by value instead of exact type
assertThat(((Number) deserializedItem).doubleValue())
.isCloseTo(((Number) originalItem).doubleValue(), within(0.0001));
} else {
assertThat(deserializedItem).isEqualTo(originalItem);
}
}
} else {
// Regular equality for other types
assertThat(deserialized).isEqualTo(obj);
}
}
} catch (Exception e) {
throw new AssertionError("Error in roundtrip for " + obj + ": " + e.getMessage(), e);
}
}
/**
* Test enum.
*/
public enum TestEnum {
VALUE1, VALUE2, VALUE3
}
/**
* Test record class.
*/
public record TestRecord(String name, int value, List<String> tags) {
}
/**
* Nested test record class.
*/
public record NestedTestRecord(String name, TestRecord child) {
}
/**
* Test class for custom object serialization.
*/
public static class TestObject {
private String name;
private int value;
private boolean active;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
/**
* Child test class for inheritance testing.
*/
public static class ChildTestObject extends TestObject {
private String childProperty;
private int childValue;
public String getChildProperty() {
return childProperty;
}
public void setChildProperty(String childProperty) {
this.childProperty = childProperty;
}
public int getChildValue() {
return childValue;
}
public void setChildValue(int childValue) {
this.childValue = childValue;
}
}
/**
* Test class with transient fields.
*/
public static class ObjectWithTransient {
private String persistent;
private transient String transientField;
public String getPersistent() {
return persistent;
}
public void setPersistent(String persistent) {
this.persistent = persistent;
}
public String getTransientField() {
return transientField;
}
public void setTransientField(String transientField) {
this.transientField = transientField;
}
}
}
+5
View File
@@ -0,0 +1,5 @@
rootProject.name = 'langgraph-java'
include 'langgraph-checkpoint'
include 'langgraph-core'
include 'langgraph-examples'