Files
langgraph/spec/JavaPortImplementationPlan.md
T
2025-03-01 18:51:13 -08:00

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)

  1. Set up project structure

    • Initialize Gradle project
    • Configure dependencies
    • Set up testing framework (JUnit 5)
  2. Implement ID utilities

    • ID.java - For deterministic ID generation
    • Test with different inputs
  3. Implement serialization framework

    • Serializer interface
    • ReflectionSerializer interface
    • TypeSerializer and TypeDeserializer interfaces
    • MsgPackSerializer implementation
    • Comprehensive tests for serialization/deserialization
  4. Implement checkpoint interfaces

    • BaseCheckpointSaver interface
    • AsyncBaseCheckpointSaver interface
    • MemoryCheckpointSaver implementation
    • Tests for checkpoint operations

Phase 2: State Management (langgraph-core)

  1. Implement channel system

    • Channel interface
    • Basic channel implementations:
      • LastValue
      • AnyValue
      • EphemeralValue
      • UntrackedValue
    • Advanced channel implementations:
      • Topic
      • BinaryOperatorAggregate
      • NamedBarrierValue
    • Tests for each channel type
  2. Implement schema validation

    • SchemaValidator interface
    • RecordSchemaValidator implementation
    • Tests for schema validation

Phase 3: Execution Engine (langgraph-core)

  1. Implement core Pregel interfaces

    • PregelProtocol interface
    • StreamMode enum
    • PregelExecutable interface
    • PregelNode class
    • PregelTask classes
    • Tests for interfaces
  2. Implement execution system

    • RetryPolicy interface
    • Checkpoint class
    • Pregel implementation
    • Tests for execution

Phase 4: High-Level API (langgraph-core)

  1. Implement StateGraph

    • GraphConstants constants
    • NodeAction and EdgeCondition interfaces
    • StateGraph class
    • CompiledStateGraph class
    • Tests for graph construction and execution
  2. Build examples

    • Counter example
    • Conversation example
    • Multi-step workflow example

Phase 5: Extensions (future)

  1. Implement Postgres adaptors
    • PostgresCheckpointSaver
    • Tests for Postgres integration

Testing Strategy

Each component will be implemented following Test-Driven Development (TDD):

  1. Write test cases based on Python implementation
  2. Implement interface to satisfy tests
  3. Implement concrete class(es)
  4. Run tests and refine implementation
  5. 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

  1. Records for state schemas

    • Java Records (Java 14+) as TypedDict/Pydantic alternative
    • Reflection-based validation and conversion
  2. CompletableFuture for async

    • Async interfaces use CompletableFuture
    • Parallel execution with work-stealing pools
  3. Generics for type safety

    • Extensive use of generics for type safety
    • Runtime type checking for dynamic aspects
  4. 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

  1. Set up project structure and build system
  2. Implement ID utilities in checkpoint module
  3. Implement MsgPackSerializer
  4. Implement MemoryCheckpointSaver
  5. Write comprehensive tests for foundation layer

Timeline

  1. Phase 1: Foundation - 2 weeks
  2. Phase 2: State Management - 2 weeks
  3. Phase 3: Execution Engine - 3 weeks
  4. Phase 4: High-Level API - 2 weeks
  5. 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.

  1. Core Data Structures & Interfaces (Week 1)

Day 1-2: Base Interfaces and Types

  1. StreamMode Enum
    • Implementation: Define values (VALUES, UPDATES, DEBUG)
    • Test: Verify serialization and string representation
  2. PregelExecutable Interface
    • Implementation: Define execute method
    • Test: Create mock implementations for testing
  3. RetryPolicy Interface & Implementations
    • Implementation: RetryPolicy interface with factory methods
    • Test: Verify retry decision logic

Day 3-4: Task Management

  1. PregelTask Class
    • Implementation: Node name, trigger, retry policy
    • Test: Constructor, getters, equals, hashCode
  2. PregelExecutableTask Class
    • Implementation: Task with inputs and context
    • Test: Construction, input/context immutability

Day 5: Core Protocol

  1. PregelProtocol Interface

    • Implementation: Define methods from spec
    • Test: Mock implementation for testing
  2. Node & Channel Integration (Week 2)

Day 1-2: Node Implementation

  1. PregelNode Class
    • Implementation: Full implementation with accessors
    • Test: Subscription, trigger, write permissions
  2. NodeRegistry
    • Implementation: Managing collections of nodes
    • Test: Registration, lookup, validation

Day 3-5: Message Handling

  1. Checkpoint Class

    • Implementation: State snapshot storage
    • Test: Capture and restore state
  2. ChannelRegistry

    • Implementation: Managing channel collections
    • Test: Registration, lookup, validation
  3. Core Execution Components (Week 3)

Day 1-2: Task Planning

  1. PregelTaskPlanner
    • Implementation: Determine tasks to execute based on updates
    • Test: Task selection with different update patterns
  2. TaskPrioritizer
    • Implementation: Order tasks for execution
    • Test: Priority ordering with different dependency patterns

Day 3-5: Task Execution

  1. TaskExecutor

    • Implementation: Execute tasks with retry logic
    • Test: Successful execution, error handling, retry behavior
  2. ExecutionContext

    • Implementation: Thread-local context for execution
    • Test: Context propagation, thread safety
  3. Superstep Management (Week 4)

Day 1-2: Superstep Core

  1. SuperstepManager
    • Implementation: Manage a single superstep execution
    • Test: Plan, execute, update phases with mock nodes
  2. UpdateCollector
    • Implementation: Collect and apply channel updates
    • Test: Update ordering, conflict resolution

Day 3-5: Execution Loop

  1. PregelLoop

    • Implementation: Core execution logic, step iteration
    • Test: Loop termination, state tracking
  2. CheckpointManager

    • Implementation: Integration with checkpointing
    • Test: Checkpoint captures, restore behavior
  3. Full Engine & Streaming (Week 5)

Day 1-3: Pregel Engine

  1. Pregel Class
    • Implementation: Core engine with all components
    • Test: End-to-end execution, configuration
  2. PregelBuilder
    • Implementation: Fluent builder interface
    • Test: Configuration options, validation

Day 4-5: Streaming Support

  1. StreamOutput
    • Implementation: Format output for streaming
    • Test: Different stream modes
  2. StreamController
    • Implementation: Manage streaming state
    • Test: Backpressure, cancellation

Implementation Strategy

  1. 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");
}
  1. 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");
}
  1. Incremental Integration

  2. Start with simplest components: PregelTask, StreamMode, etc.

  3. Build TaskPlanner with mocked nodes

  4. Create TaskExecutor with mocked actions

  5. Integrate into SuperstepManager

  6. Combine in PregelLoop

  7. 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
}
  1. Use Real Components When Possible

  2. Use real Channel implementations from previous phase

  3. Create simple test PregelExecutables

  4. 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:

  1. Unit Testing: Test each component in isolation
  2. Component Testing: Test related components together
  3. Integration Testing: Test main subsystems
  4. System Testing: Test complete workflows

Example Test Progression for TaskPlanner:

  1. 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
}
  1. 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
}
  1. 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
}
  1. 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:

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