Java 17+ SDK for the Pando AI coding assistant.
Pando can operate in three modes — all three are supported by this SDK:
| Mode | Class | Description |
|---|---|---|
| Subprocess | PandoClient |
One-shot pando -p "..." runs with JSON output |
| ACP stdio | PandoAgent / PandoSession |
Long-lived JSON-RPC 2.0 session over stdin/stdout |
| HTTP REST | PandoHttpClient |
Connects to a running pando serve or pando app instance |
- Java 17 or later
- Pando CLI installed and on your
PATH(or setPANDO_PATHenvironment variable)
Add to your pom.xml:
<dependency>
<groupId>io.pando</groupId>
<artifactId>pando-sdk</artifactId>
<version>0.1.0</version>
</dependency>Or Gradle (build.gradle.kts):
implementation("io.pando:pando-sdk:0.1.0")import io.pando.sdk.PandoClient;
import io.pando.sdk.model.RunResult;
PandoClient client = PandoClient.builder()
.cwd("/path/to/project")
.model("copilot.gpt-5.4")
.timeout(java.time.Duration.ofMinutes(5))
.build();
RunResult result = client.run("Fix all lint errors");
System.out.println(result.response());import io.pando.sdk.PandoClient.RunOptions;
RunResult result = client.run("Refactor auth module", RunOptions.builder()
.allowAllTools(true) // passes --yolo to auto-approve tool permissions
.model("claude-sonnet-4-6") // override model for this run
.build());client.runAsync("Fix lint errors")
.thenAccept(r -> System.out.println(r.response()))
.join();client.stream("Explain the code", line -> System.out.print(line + "\n"));import io.pando.sdk.PandoAgent;
import io.pando.sdk.PandoSession;
try (PandoAgent agent = PandoAgent.builder()
.cwd("/path/to/project")
.model("claude-sonnet-4-6")
.persona("software-engineer")
.build()) {
agent.connect();
PandoSession session = agent.createSession("Refactoring task");
String response = session.ask("Refactor the database layer");
System.out.println(response);
}import io.pando.sdk.events.*;
import java.util.concurrent.Flow;
session.send("Explain the codebase").subscribe(new Flow.Subscriber<AgentEvent>() {
@Override
public void onSubscribe(Flow.Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(AgentEvent event) {
switch (event) {
case ContentDeltaEvent e -> System.out.print(e.delta());
case ToolCallEvent e -> System.out.println("\n[Tool] " + e.toolCall().name());
case ToolResultEvent e -> System.out.println("[Result] " + e.toolResult().content());
case ResponseEvent e -> System.out.println("\n[Done]");
case ErrorEvent e -> System.err.println("[Error] " + e.error());
case ThinkingDeltaEvent e -> {} // reasoning content
case SummarizeEvent e -> System.out.println("[Context summarized]");
}
}
@Override
public void onError(Throwable t) { t.printStackTrace(); }
@Override
public void onComplete() {}
});agent.createSessionAsync("Task")
.thenCompose(s -> s.askAsync("Fix lint errors"))
.thenAccept(System.out::println)
.join();PandoAgent agent = PandoAgent.builder()
.cwd("/project")
.toolPermissionHandler(req -> {
// Deny bash tool, approve everything else
return !req.toolName().equals("bash");
})
.build();// Set on agent (affects all future sessions)
agent.setPersona("qa");
// Set on a specific session
session.setPersona("system-engineer");
// List available personas
List<String> personas = agent.listPersonas();
// ["assistant", "software-engineer", "qa", "system-engineer"]Connects to a running pando serve or pando app instance.
import io.pando.sdk.PandoHttpClient;
import io.pando.sdk.model.SessionInfo;
PandoHttpClient client = PandoHttpClient.builder()
.baseUrl("http://localhost:8765")
.disableSslVerification(true) // for self-signed dev certificates
.timeout(java.time.Duration.ofSeconds(60))
.build();
// Create a session
SessionInfo session = client.sessions().create("My task");
// Send a message and get full response
String response = client.sessions().ask(session.sessionId(), "Fix lint errors");
System.out.println(response);
// Stream the response
client.sessions().sendMessage(session.sessionId(), "Explain this code")
.subscribe(new Flow.Subscriber<PandoHttpClient.HttpStreamChunk>() {
public void onSubscribe(Flow.Subscription s) { s.request(Long.MAX_VALUE); }
public void onNext(PandoHttpClient.HttpStreamChunk chunk) {
if (chunk.delta() != null) System.out.print(chunk.delta());
}
public void onError(Throwable t) { t.printStackTrace(); }
public void onComplete() {}
});
// List sessions
List<SessionInfo> sessions = client.sessions().list();
// Models
List<io.pando.sdk.model.ModelInfo> models = client.models().list();
client.models().setActive("claude-sonnet-4-6");
// Personas
List<String> personas = client.personas().list();AG-UI is the protocol CopilotKit and other Generative-UI
frontends speak to agent backends. Pando serves it from pando agui-serve --port 8090
(or pando serve --agui-port 8090); it is off by default and requires a bearer
token and an origin allow-list, because it exposes a code-executing agent to a browser.
The client lives in io.pando.sdk.agui and uses the built-in java.net.http.HttpClient
plus Jackson — no extra dependency.
import io.pando.sdk.agui.*;
PandoAguiClient client = PandoAguiClient.builder()
.baseUrl("http://localhost:8090")
.token(System.getenv("PANDO_TOKEN"))
.agent("coder")
.build();
// Discovery: which agents exist, their model, which capabilities are on
AguiInfo info = client.info();
client.run(RunOptions.prompt("Summarise the repo"), event -> {
switch (event.type()) {
case "TEXT_MESSAGE_CONTENT" -> System.out.print(event.string("delta"));
case "STATE_SNAPSHOT" -> {
PandoState state = event.snapshot().orElseThrow();
System.out.println(state.todos() + " " + state.subAgents());
}
case "RUN_FINISHED" -> {
if (AguiEvent.OUTCOME_INTERRUPT.equals(event.string("outcome"))) {
// The agent called one of your tools: run it, then run again on the
// same thread with AguiMessage.toolResult(...), which resumes it.
}
}
default -> { }
}
});
String text = client.runText("What does cmd/root.go do?");Reactive streaming uses the same Flow.Publisher idiom as PandoHttpClient:
Flow.Publisher<AguiEvent> events = client.stream(RunOptions.prompt("Refactor the parser"));Frontend tools and human-in-the-loop approvals:
RunOptions options = RunOptions.builder()
.message(AguiMessage.user("chart the commits"))
.tool(AguiTool.of("show_chart", "Renders a chart", Map.of("type", "object")))
.threadId(threadId)
.build();
// A TOOL_CALL_START whose name is PandoAguiClient.PERMISSION_TOOL_NAME is an approval
// prompt; parse its arguments with PandoPermissionRequest.fromArguments(args) and answer
// with AguiMessage.toolResult(callId, PandoPermissionRequest.answer(true)).| Type | Purpose |
|---|---|
PandoAguiClient |
Run/discovery client (run, stream, runText, info) |
AguiEvent |
One protocol event, with snapshot() and delta() helpers |
RunOptions |
Builder for a run (prompt or transcript, tools, context, state, agent) |
PandoState |
The shared-state document (STATE_SNAPSHOT) |
AguiMessage / AguiTool / AguiContext |
Request payload types |
AguiInfo |
The /info discovery document |
PandoAguiClient.parseSse |
The event-stream parser, if you issue the request yourself |
PandoPermissionRequest |
Human-in-the-loop approvals |
All exceptions extend PandoException (unchecked):
import io.pando.sdk.exception.*;
try {
RunResult result = client.run("Fix errors");
} catch (PandoBinaryNotFoundException e) {
System.err.println("Install pando: " + e.getMessage());
System.err.println("Searched: " + e.getSearchedPaths());
} catch (PandoTimeoutException e) {
System.err.println("Timed out after: " + e.getTimeout());
} catch (PandoConnectionException e) {
System.err.println("Process error (exit " + e.getExitCode() + "): " + e.getMessage());
} catch (PandoRpcException e) {
System.err.println("RPC error " + e.getCode() + ": " + e.getRpcMessage());
} catch (PandoSessionException e) {
System.err.println("Session error: " + e.getMessage());
} catch (PandoException e) {
System.err.println("Pando error: " + e.getMessage());
}Exception hierarchy:
PandoException (extends RuntimeException)
├── PandoBinaryNotFoundException — pando binary not found
├── PandoConnectionException — process died or I/O failure
├── PandoSessionException — invalid session state
├── PandoTimeoutException — operation timed out
└── PandoRpcException — JSON-RPC error response
JsonRpcTransportis fully thread-safe. MultiplePandoSessions can send concurrent requests through the same underlying transport without synchronization on the caller side.PandoAgentandPandoClientare thread-safe after construction.PandoSession.send()creates a freshSubmissionPublisherper call. Each publisher is independent and can have multiple subscribers.- The background reader thread in
JsonRpcTransportis a daemon thread and does not prevent JVM shutdown.
The binary is resolved in this order:
- Explicit
pandoPath(...)on the builder. PANDO_PATHenvironment variable.- Directories listed in the system
PATH.
On Windows, both pando and pando.exe are checked.
// Use a specific binary
PandoClient.builder().pandoPath("/usr/local/bin/pando").build();
// Or set env var
// export PANDO_PATH=/opt/pando/bin/pandocd sdk/java
mvn clean package
mvn testRequires Java 17+ and Maven 3.8+.