Skip to content

Latest commit

 

History

History

README.md

HotPlex Java Client

Java client SDK for HotPlex Gateway

Maven Central License


Status

🚧 Under Development - API may change


Requirements

  • Java 17+
  • Maven 3.8+ or Gradle 8+

Installation

Maven

<dependency>
    <groupId>dev.hotplex</groupId>
    <artifactId>hotplex-client</artifactId>
    <version>1.1.0</version>
</dependency>

Gradle (Groovy)

implementation 'dev.hotplex:hotplex-client:1.1.0'

Gradle (Kotlin)

implementation("dev.hotplex:hotplex-client:1.1.0")

Quick Start

import dev.hotplex.client.*;
import dev.hotplex.protocol.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // Create client
        HotPlexClient client = HotPlexClient.builder()
            .url("ws://localhost:8888")
            .workerType(WorkerType.CLAUDE_CODE)
            .authToken("your-api-key")
            .build();

        // Register event listeners
        client.onMessageDelta(data -> {
            System.out.print(data.getContent());
        });

        client.onDone(data -> {
            System.out.printf("%n✅ Done! Success: %b%n", data.isSuccess());
            if (data.getStats() != null) {
                System.out.printf("   Duration: %dms%n", data.getStats().getDurationMs());
                System.out.printf("   Tokens: %d%n", data.getStats().getTotalTokens());
            }
        });

        client.onError(data -> {
            System.err.printf("Error [%s]: %s%n", data.getCode(), data.getMessage());
        });

        // Connect
        try {
            client.connect();
            System.out.println("Connected! Session: " + client.getSessionId());

            // Send input
            client.sendInput(InputData.builder()
                .content("Write a hello world in Java")
                .build());

            // Wait for completion
            Thread.sleep(30000);
        } finally {
            client.close();
        }
    }
}

API Reference

Builder

HotPlexClient client = HotPlexClient.builder()
    .url("ws://localhost:8888")                    // Required
    .workerType(WorkerType.CLAUDE_CODE)            // Required
    .authToken("your-api-key")                     // Optional
    .sessionId("existing-session-id")              // Optional (resume session)
    .reconnect(true)                               // Optional (default: true)
    .reconnectMaxAttempts(5)                       // Optional (default: 5)
    .timeout(Duration.ofSeconds(30))               // Optional (default: 30s)
    .metadata(Map.of("key", "value"))              // Optional
    .build();

Connection Methods

// Connect establishes WebSocket connection and initializes session
client.connect();

// Close closes connection and cleanup resources
client.close();

// GetSessionId returns current session ID
String sessionId = client.getSessionId();

// IsConnected returns connection status
boolean connected = client.isConnected();

Sending Messages

Input

client.sendInput(InputData.builder()
    .content("Your input here")
    .metadata(Map.of("language", "java"))
    .build());

Tool Result

client.sendToolResult(ToolResultData.builder()
    .toolCallId("call_123")
    .output("result")
    .error(null) // optional
    .build());

Permission Response

client.sendPermissionResponse(PermissionResponseData.builder()
    .permissionId("perm_456")
    .allowed(true)
    .reason("User approved")
    .build());

Event Listeners

Message Events

// Message start
client.onMessageStart(data -> {
    System.out.println("Message started: " + data.getId());
});

// Message delta (streaming)
client.onMessageDelta(data -> {
    System.out.print(data.getContent());
});

// Message end
client.onMessageEnd(data -> {
    System.out.println("Message ended: " + data.getId());
});

Tool Events

client.onToolCall(data -> {
    // Execute tool
    String result = executeTool(data.getName(), data.getInput());

    // Send result back
    client.sendToolResult(ToolResultData.builder()
        .toolCallId(data.getId())
        .output(result)
        .build());
});

Permission Events

client.onPermissionRequest(data -> {
    boolean allowed = askUser(data.getToolName(), data.getDescription());

    client.sendPermissionResponse(PermissionResponseData.builder()
        .permissionId(data.getId())
        .allowed(allowed)
        .reason(allowed ? "User approved" : "User denied")
        .build());
});

Lifecycle Events

// State change
client.onState(data -> {
    System.out.println("State: " + data.getState());

    if (data.getState() == SessionState.IDLE) {
        System.out.println("Worker idle, ready for input");
    }
});

// Completion
client.onDone(data -> {
    System.out.println("Done! Success: " + data.isSuccess());
});

// Error
client.onError(data -> {
    System.err.println("Error: " + data.getCode() + " - " + data.getMessage());
});

Connection Events

client.onConnected(() -> {
    System.out.println("WebSocket connected");
});

client.onDisconnected(() -> {
    System.out.println("WebSocket disconnected");
});

client.onReconnecting((attempt, maxAttempts) -> {
    System.out.printf("Reconnecting %d/%d%n", attempt, maxAttempts);
});

Data Classes

InputData

InputData data = InputData.builder()
    .content("Your input")
    .metadata(Map.of("key", "value"))
    .build();

MessageDeltaData

String content = data.getContent();

ToolCallData

String id = data.getId();
String name = data.getName();
Map<String, Object> input = data.getInput();

DoneData

boolean success = data.isSuccess();
DoneStats stats = data.getStats();

if (stats != null) {
    int durationMs = stats.getDurationMs();
    int totalTokens = stats.getTotalTokens();
    double costUsd = stats.getCostUsd();
}

ErrorData

String code = data.getCode();
String message = data.getMessage();
Map<String, Object> details = data.getDetails();

Advanced Usage

Session Resumption

// First session
HotPlexClient client1 = HotPlexClient.builder()
    .url("ws://localhost:8888")
    .workerType(WorkerType.CLAUDE_CODE)
    .authToken("your-key")
    .build();

client1.connect();
String sessionId = client1.getSessionId();
client1.sendInput(InputData.builder()
    .content("Start task...")
    .build());

// Later: resume session
HotPlexClient client2 = HotPlexClient.builder()
    .url("ws://localhost:8888")
    .workerType(WorkerType.CLAUDE_CODE)
    .authToken("your-key")
    .sessionId(sessionId) // Resume
    .build();

client2.connect();
client2.sendInput(InputData.builder()
    .content("Continue task...")
    .build());

Async Pattern with CompletableFuture

import java.util.concurrent.CompletableFuture;

public CompletableFuture<Void> sendInputAsync(String content) {
    CompletableFuture<Void> future = new CompletableFuture<>();

    client.onDone(data -> {
        if (data.isSuccess()) {
            future.complete(null);
        } else {
            future.completeExceptionally(new RuntimeException("Task failed"));
        }
    });

    client.onError(data -> {
        future.completeExceptionally(new RuntimeException(data.getMessage()));
    });

    client.sendInput(InputData.builder()
        .content(content)
        .build());

    return future;
}

// Usage
sendInputAsync("Write hello world")
    .thenRun(() -> System.out.println("Task completed!"))
    .exceptionally(err -> {
        System.err.println("Task failed: " + err.getMessage());
        return null;
    });

Tool Implementation

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

client.onToolCall(data -> {
    try {
        String output;

        switch (data.getName()) {
            case "bash":
                output = executeBash(data.getInput().get("command").toString());
                break;

            case "read_file":
                String path = data.getInput().get("path").toString();
                output = Files.readString(Paths.get(path));
                break;

            default:
                throw new IllegalArgumentException("Unknown tool: " + data.getName());
        }

        client.sendToolResult(ToolResultData.builder()
            .toolCallId(data.getId())
            .output(output)
            .build());

    } catch (Exception err) {
        client.sendToolResult(ToolResultData.builder()
            .toolCallId(data.getId())
            .output("")
            .error(err.getMessage())
            .build());
    }
});

Graceful Shutdown

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("\nShutting down...");
    try {
        client.close();
        System.out.println("Client closed");
    } catch (Exception err) {
        System.err.println("Error during shutdown: " + err.getMessage());
    }
}));

Error Handling

Exception Hierarchy

HotPlexException (base)
├── ConnectionException
├── SessionException
└── TimeoutException

Handling Errors

import dev.hotplex.client.exception.*;

try {
    client.connect();
} catch (ConnectionException err) {
    System.err.println("Connection failed: " + err.getMessage());
} catch (SessionException err) {
    System.err.println("Session error: " + err.getMessage());
} catch (HotPlexException err) {
    System.err.println("HotPlex error: " + err.getMessage());
}

Common Error Codes

Code Meaning Action
SESSION_NOT_FOUND Session doesn't exist Create new session
SESSION_TERMINATED Session terminated Create new session
UNAUTHORIZED Invalid auth token Check token
INVALID_INPUT Malformed input Check message format

Testing

Unit Tests

mvn test

Integration Tests

# Start gateway
./hotplex &

# Run integration tests
mvn verify -Pintegration-test

Test Example

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ClientTest {
    @Test
    void testConnection() throws Exception {
        HotPlexClient client = HotPlexClient.builder()
            .url("ws://localhost:8888")
            .workerType(WorkerType.CLAUDE_CODE)
            .authToken("test-key")
            .build();

        assertDoesNotThrow(() -> client.connect());
        assertNotNull(client.getSessionId());

        client.close();
    }
}

Architecture

┌─────────────────────────────────────────┐
│         HotPlexClient                   │
│  - Event listeners (on*)                │
│  - Message builders (send*)             │
│  - State management                     │
├─────────────────────────────────────────┤
│         Transport (WebSocket)           │
│  - Connection lifecycle                 │
│  - Auto-reconnect with backoff          │
│  - Message queue                        │
├─────────────────────────────────────────┤
│         Protocol (AEP v1)               │
│  - NDJSON codec                         │
│  - Envelope builder                     │
│  - Event type definitions               │
└─────────────────────────────────────────┘

Package Structure:

dev.hotplex.client         # High-level client API
dev.hotplex.protocol       # AEP codec and types
dev.hotplex.exception      # Exception hierarchy

Development

Build

mvn clean package

Test

mvn test

Install Locally

mvn clean install

License

Apache-2.0


Related

  • Protocol Spec: docs/architecture/AEP-v1-Protocol.md
  • Python Client: examples/python-client/
  • TypeScript Client: examples/typescript-client/
  • Go Client: client/