19 KiB
Java Port Implementation Plan for LangGraph
This document outlines the plan for implementing a Java port of LangGraph, following the specifications defined in the other documents.
Project Structure
langgraph-java/
├── build.gradle
├── settings.gradle
├── README.md
├── langgraph-checkpoint/ # Base persistence interfaces (equiv. to libs/checkpoint)
│ └── src/
│ ├── main/java/
│ │ └── com/langgraph/checkpoint/
│ │ ├── base/ # Base interfaces and utilities
│ │ └── serde/ # Serialization
│ └── test/java/
├── langgraph-core/ # Main library (equiv. to libs/langgraph)
│ └── src/
│ ├── main/java/
│ │ └── com/langgraph/
│ │ ├── channels/ # Channel implementations
│ │ ├── graph/ # StateGraph API
│ │ └── pregel/ # Pregel execution engine
│ └── test/java/
├── langgraph-examples/ # Example applications
│ └── src/
│ └── main/java/
│ └── com/langgraph/examples/
└── langgraph-checkpoint-postgres/ # Postgres implementation (future)
Development Phases
The implementation will follow a bottom-up approach, starting with the lowest-level components and building up to the high-level APIs.
Phase 1: Foundation (langgraph-checkpoint)
-
Set up project structure
- Initialize Gradle project
- Configure dependencies
- Set up testing framework (JUnit 5)
-
Implement ID utilities
ID.java- For deterministic ID generation- Test with different inputs
-
Implement serialization framework
SerializerinterfaceReflectionSerializerinterfaceTypeSerializerandTypeDeserializerinterfacesMsgPackSerializerimplementation- Comprehensive tests for serialization/deserialization
-
Implement checkpoint interfaces
BaseCheckpointSaverinterfaceAsyncBaseCheckpointSaverinterfaceMemoryCheckpointSaverimplementation- Tests for checkpoint operations
Phase 2: State Management (langgraph-core)
-
Implement channel system
Channelinterface- Basic channel implementations:
LastValueAnyValueEphemeralValueUntrackedValue
- Advanced channel implementations:
TopicBinaryOperatorAggregateNamedBarrierValue
- Tests for each channel type
-
Implement schema validation
SchemaValidatorinterfaceRecordSchemaValidatorimplementation- Tests for schema validation
Phase 3: Execution Engine (langgraph-core)
-
Implement core Pregel interfaces
PregelProtocolinterfaceStreamModeenumPregelExecutableinterfacePregelNodeclassPregelTaskclasses- Tests for interfaces
-
Implement execution system
RetryPolicyinterfaceCheckpointclassPregelimplementation- Tests for execution
Phase 4: High-Level API (langgraph-core)
-
Implement StateGraph
GraphConstantsconstantsNodeActionandEdgeConditioninterfacesStateGraphclassCompiledStateGraphclass- Tests for graph construction and execution
-
Build examples
- Counter example
- Conversation example
- Multi-step workflow example
Phase 5: Extensions (future)
- Implement Postgres adaptors
- PostgresCheckpointSaver
- Tests for Postgres integration
Testing Strategy
Each component will be implemented following Test-Driven Development (TDD):
- Write test cases based on Python implementation
- Implement interface to satisfy tests
- Implement concrete class(es)
- Run tests and refine implementation
- Document completed component
Key Test Areas
- Serialization: Test with various object types, nested structures, and cycles
- Channels: Test all channel behaviors and edge cases
- Pregel: Test deterministic execution, error handling, and checkpoint integration
- StateGraph: Test graph construction, validation, and execution
Implementation Notes
Java-Specific Adaptations
-
Records for state schemas
- Java Records (Java 14+) as TypedDict/Pydantic alternative
- Reflection-based validation and conversion
-
CompletableFuture for async
- Async interfaces use CompletableFuture
- Parallel execution with work-stealing pools
-
Generics for type safety
- Extensive use of generics for type safety
- Runtime type checking for dynamic aspects
-
Builder pattern
- Builder pattern for complex object construction
- Fluent interfaces for API usability
Performance Considerations
- Thread-safe implementations using concurrent collections
- Atomic operations for parallel safety
- Minimizing object creation and copying
- Efficient serialization with MessagePack
Initial Implementation Tasks
- Set up project structure and build system
- Implement ID utilities in checkpoint module
- Implement MsgPackSerializer
- Implement MemoryCheckpointSaver
- Write comprehensive tests for foundation layer
Timeline
- Phase 1: Foundation - 2 weeks
- Phase 2: State Management - 2 weeks
- Phase 3: Execution Engine - 3 weeks
- Phase 4: High-Level API - 2 weeks
- Integration and Examples - 1 week
Total estimated time: ~10 weeks for core functionality
Dependencies
- MessagePack:
org.msgpack:msgpack-core:0.9.3 - JUnit 5:
org.junit.jupiter:junit-jupiter:5.8.2 - Mockito:
org.mockito:mockito-core:4.5.1 - AssertJ:
org.assertj:assertj-core:3.22.0
Phase 3 in detail:
This implementation plan is organized to match the structure of the Python implementation while prioritizing testability at each step. Don't forget to use Java patterns and idioms where appropriate.
- Core Data Structures & Interfaces (Week 1)
Day 1-2: Base Interfaces and Types
- StreamMode Enum
- Implementation: Define values (VALUES, UPDATES, DEBUG)
- Test: Verify serialization and string representation
- PregelExecutable Interface
- Implementation: Define execute method
- Test: Create mock implementations for testing
- RetryPolicy Interface & Implementations
- Implementation: RetryPolicy interface with factory methods
- Test: Verify retry decision logic
Day 3-4: Task Management
- PregelTask Class
- Implementation: Node name, trigger, retry policy
- Test: Constructor, getters, equals, hashCode
- PregelExecutableTask Class
- Implementation: Task with inputs and context
- Test: Construction, input/context immutability
Day 5: Core Protocol
-
PregelProtocol Interface
- Implementation: Define methods from spec
- Test: Mock implementation for testing
-
Node & Channel Integration (Week 2)
Day 1-2: Node Implementation
- PregelNode Class
- Implementation: Full implementation with accessors
- Test: Subscription, trigger, write permissions
- NodeRegistry
- Implementation: Managing collections of nodes
- Test: Registration, lookup, validation
Day 3-5: Message Handling
-
Checkpoint Class
- Implementation: State snapshot storage
- Test: Capture and restore state
-
ChannelRegistry
- Implementation: Managing channel collections
- Test: Registration, lookup, validation
-
Core Execution Components (Week 3)
Day 1-2: Task Planning
- PregelTaskPlanner
- Implementation: Determine tasks to execute based on updates
- Test: Task selection with different update patterns
- TaskPrioritizer
- Implementation: Order tasks for execution
- Test: Priority ordering with different dependency patterns
Day 3-5: Task Execution
-
TaskExecutor
- Implementation: Execute tasks with retry logic
- Test: Successful execution, error handling, retry behavior
-
ExecutionContext
- Implementation: Thread-local context for execution
- Test: Context propagation, thread safety
-
Superstep Management (Week 4)
Day 1-2: Superstep Core
- SuperstepManager
- Implementation: Manage a single superstep execution
- Test: Plan, execute, update phases with mock nodes
- UpdateCollector
- Implementation: Collect and apply channel updates
- Test: Update ordering, conflict resolution
Day 3-5: Execution Loop
-
PregelLoop
- Implementation: Core execution logic, step iteration
- Test: Loop termination, state tracking
-
CheckpointManager
- Implementation: Integration with checkpointing
- Test: Checkpoint captures, restore behavior
-
Full Engine & Streaming (Week 5)
Day 1-3: Pregel Engine
- Pregel Class
- Implementation: Core engine with all components
- Test: End-to-end execution, configuration
- PregelBuilder
- Implementation: Fluent builder interface
- Test: Configuration options, validation
Day 4-5: Streaming Support
- StreamOutput
- Implementation: Format output for streaming
- Test: Different stream modes
- StreamController
- Implementation: Manage streaming state
- Test: Backpressure, cancellation
Implementation Strategy
- Incremental Testing
Create test classes for each component that can be used in isolation:
@Test
void testTaskPlanning() {
// Create mock channels with updates
Map<String, Channel> channels = createMockChannels(
Map.of("input", true, "other", false));
// Create nodes with subscriptions
Set<PregelNode> nodes = createTestNodes();
// Create planner
PregelTaskPlanner planner = new PregelTaskPlanner(nodes);
// Test planning logic
List<PregelTask> tasks = planner.plan(channels);
// Verify correct tasks selected
assertThat(tasks).hasSize(1);
assertThat(tasks.get(0).getNode()).isEqualTo("processor");
}
- Test Each Component in Isolation
For each component, test:
- Normal operation
- Edge cases
- Error conditions
- Integration with dependencies
@Test
void testTaskExecution() {
// Create mock executor
TaskExecutor executor = new TaskExecutor();
// Create task with expected inputs
PregelExecutableTask task = createTestTask();
// Execute and capture results
Map<String, Object> result = executor.execute(task);
// Verify results
assertThat(result)
.containsKey("output")
.containsEntry("output", "processed");
}
-
Incremental Integration
-
Start with simplest components: PregelTask, StreamMode, etc.
-
Build TaskPlanner with mocked nodes
-
Create TaskExecutor with mocked actions
-
Integrate into SuperstepManager
-
Combine in PregelLoop
-
Build complete Pregel engine
// First, test task planner alone
@Test
void testTaskPlannerInIsolation() {
PregelTaskPlanner planner = new PregelTaskPlanner(mockNodes);
List<PregelTask> tasks = planner.plan(updatedChannels);
// Verify tasks
}
// Then, test executor alone
@Test
void testTaskExecutorInIsolation() {
TaskExecutor executor = new TaskExecutor();
Map<String, Object> result = executor.execute(mockTask);
// Verify result
}
// Finally, test them together in SuperstepManager
@Test
void testSuperstepIntegration() {
SuperstepManager manager = new SuperstepManager(
planner,
executor,
channels
);
SuperstepResult result = manager.executeStep();
// Verify complete superstep behavior
}
-
Use Real Components When Possible
-
Use real Channel implementations from previous phase
-
Create simple test PregelExecutables
-
Build test workflows of increasing complexity
@Test
void testSimpleWorkflow() {
// Create real channels
Map<String, Channel> channels = new HashMap<>();
channels.put("input", new LastValue<>(String.class));
channels.put("output", new LastValue<>(String.class));
// Create real nodes
Map<String, PregelNode> nodes = new HashMap<>();
nodes.put("processor", new PregelNode(
"processor",
(inputs, context) -> {
String input = (String) inputs.get("input");
return Map.of("output", input.toUpperCase());
},
Set.of("input"),
null,
Set.of("output"),
null
));
// Create real Pregel instance
Pregel pregel = new Pregel(nodes, channels, null);
// Run and verify
Object result = pregel.invoke(Map.of("input", "hello"), null);
// Verify complete execution
@SuppressWarnings("unchecked")
Map<String, Object> resultMap = (Map<String, Object>) result;
assertThat(resultMap).containsEntry("output", "HELLO");
}
File Organization
Based on the Python structure, here's how the Java implementation will be organized:
com.langgraph.pregel/ ├── PregelProtocol.java # Core interface ├── StreamMode.java # Enum for streaming options ├── PregelExecutable.java # Interface for node functions ├── PregelNode.java # Node definition ├── task/ │ ├── PregelTask.java # Task representation │ ├── PregelExecutableTask.java # Task with inputs │ ├── TaskPlanner.java # Task planning logic │ └── TaskExecutor.java # Task execution ├── state/ │ ├── Checkpoint.java # State checkpoint │ ├── ChannelRegistry.java # Channel management │ └── NodeRegistry.java # Node management ├── execute/ │ ├── SuperstepManager.java # Single superstep execution │ ├── PregelLoop.java # Main execution loop │ ├── ExecutionContext.java # Context for execution │ └── UpdateCollector.java # Collect updates ├── stream/ │ ├── StreamController.java # Manage streaming │ └── StreamOutput.java # Format output ├── retry/ │ ├── RetryPolicy.java # Retry interface │ └── RetryPolicies.java # Standard policies └── Pregel.java # Main implementation
Testing Step-by-Step
The testing strategy follows a specific progression:
- Unit Testing: Test each component in isolation
- Component Testing: Test related components together
- Integration Testing: Test main subsystems
- System Testing: Test complete workflows
Example Test Progression for TaskPlanner:
- Unit Test: Mock everything
@Test
void testPlannerWithMocks() {
Set<String> updatedChannels = Set.of("input");
Map<String, PregelNode> mockNodes = createMockNodes();
TaskPlanner planner = new TaskPlanner(mockNodes);
List<PregelTask> tasks = planner.plan(updatedChannels);
// Test with various update patterns
}
- Component Test: Use real nodes, mock channels
@Test
void testPlannerWithRealNodes() {
Set<String> updatedChannels = Set.of("input");
Map<String, PregelNode> realNodes = createRealNodes();
TaskPlanner planner = new TaskPlanner(realNodes);
List<PregelTask> tasks = planner.plan(updatedChannels);
// Verify with real node behavior
}
- Integration Test: Use real nodes and channels
@Test
void testPlannerIntegration() {
Map<String, Channel> channels = createRealChannels();
// Update channels
channels.get("input").update("test");
Map<String, PregelNode> nodes = createRealNodes();
// Get updated channel names
Set<String> updatedChannels = getUpdatedChannelNames(channels);
TaskPlanner planner = new TaskPlanner(nodes);
List<PregelTask> tasks = planner.plan(updatedChannels);
// Verify end-to-end planning
}
- System Test: Use in a full Pregel execution
@Test
void testPlannerInFullSystem() {
// Set up complete Pregel system
Pregel pregel = createTestPregelSystem();
// Execute a workflow that will trigger planning
pregel.invoke(Map.of("input", "test"), null);
// Verify entire execution via output
}
Sample Test Case Implementations
To illustrate the TDD approach, here are key test cases for early components:
- PregelTask
@Test
void testPregelTask() {
// Basic construction
PregelTask task = new PregelTask("node1", "trigger1", RetryPolicy.noRetry());
assertThat(task.getNode()).isEqualTo("node1");
assertThat(task.getTrigger()).isEqualTo("trigger1");
assertThat(task.getRetryPolicy()).isNotNull();
// Equality
PregelTask sameTask = new PregelTask("node1", "trigger1", RetryPolicy.maxAttempts(3));
PregelTask differentNode = new PregelTask("node2", "trigger1", RetryPolicy.noRetry());
PregelTask differentTrigger = new PregelTask("node1", "trigger2", RetryPolicy.noRetry());
assertThat(task).isEqualTo(sameTask);
assertThat(task).isNotEqualTo(differentNode);
assertThat(task).isNotEqualTo(differentTrigger);
}
- PregelNode
@Test
void testPregelNode() {
// Create a simple action
PregelExecutable action = (inputs, context) -> Map.of("output", "result");
// Basic construction
PregelNode node = new PregelNode(
"processor",
action,
Set.of("input1", "input2"),
"trigger1",
Set.of("output1", "output2"),
RetryPolicy.maxAttempts(3)
);
// Test properties
assertThat(node.getName()).isEqualTo("processor");
assertThat(node.getAction()).isSameAs(action);
assertThat(node.getSubscribe()).containsExactlyInAnyOrder("input1", "input2");
assertThat(node.getTrigger()).isEqualTo("trigger1");
assertThat(node.getWriters()).containsExactlyInAnyOrder("output1", "output2");
assertThat(node.getRetryPolicy()).isNotNull();
// Test helper methods
assertThat(node.subscribesTo("input1")).isTrue();
assertThat(node.subscribesTo("input3")).isFalse();
assertThat(node.hasTrigger("trigger1")).isTrue();
assertThat(node.hasTrigger("trigger2")).isFalse();
assertThat(node.canWriteTo("output1")).isTrue();
assertThat(node.canWriteTo("output3")).isFalse();
}
- TaskPlanner
@Test
void testTaskPlanner() {
// Create nodes
PregelNode node1 = new PregelNode(
"node1",
(inputs, context) -> Map.of(),
Set.of("channel1"),
null,
Set.of("output1"),
null
);
PregelNode node2 = new PregelNode(
"node2",
(inputs, context) -> Map.of(),
Set.of("channel2"),
null,
Set.of("output2"),
null
);
PregelNode node3 = new PregelNode(
"node3",
(inputs, context) -> Map.of(),
null,
"trigger1",
Set.of("output3"),
null
);
Map<String, PregelNode> nodes = Map.of(
"node1", node1,
"node2", node2,
"node3", node3
);
TaskPlanner planner = new TaskPlanner(nodes);
// Test with different updated channels
Set<String> update1 = Set.of("channel1");
List<PregelTask> tasks1 = planner.plan(update1);
assertThat(tasks1).hasSize(1);
assertThat(tasks1.get(0).getNode()).isEqualTo("node1");
Set<String> update2 = Set.of("channel1", "channel2");
List<PregelTask> tasks2 = planner.plan(update2);
assertThat(tasks2).hasSize(2);
Set<String> update3 = Set.of("trigger1");
List<PregelTask> tasks3 = planner.plan(update3);
assertThat(tasks3).hasSize(1);
assertThat(tasks3.get(0).getNode()).isEqualTo("node3");
Set<String> update4 = Set.of("channel3");
List<PregelTask> tasks4 = planner.plan(update4);
assertThat(tasks4).isEmpty();
}