Skip to main content
Complete API reference for the ACP Java SDK, covering client, agent (all three styles), protocol types, transports, errors, and test utilities.

Installation

Snapshot builds are published to Maven Central Snapshots. Add the snapshot repository to your build.

Maven

Add the snapshot repository, then the dependency:
<repositories>
    <repository>
        <id>central-snapshots</id>
        <url>https://central.sonatype.com/repository/maven-snapshots/</url>
        <snapshots><enabled>true</enabled></snapshots>
        <releases><enabled>false</enabled></releases>
    </repository>
</repositories>
Core SDK (client + sync/async agent APIs):
<dependency>
    <groupId>com.agentclientprotocol</groupId>
    <artifactId>acp-core</artifactId>
    <version>0.9.0-SNAPSHOT</version>
</dependency>
Annotation-based agent support (includes acp-core transitively):
<dependency>
    <groupId>com.agentclientprotocol</groupId>
    <artifactId>acp-agent-support</artifactId>
    <version>0.9.0-SNAPSHOT</version>
</dependency>
Test utilities:
<dependency>
    <groupId>com.agentclientprotocol</groupId>
    <artifactId>acp-test</artifactId>
    <version>0.9.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
WebSocket server transport for agents:
<dependency>
    <groupId>com.agentclientprotocol</groupId>
    <artifactId>acp-websocket-jetty</artifactId>
    <version>0.9.0-SNAPSHOT</version>
</dependency>

Gradle

Add the snapshot repository:
// build.gradle
repositories {
    maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
}

implementation 'com.agentclientprotocol:acp-core:0.9.0-SNAPSHOT'

// Optional modules
implementation 'com.agentclientprotocol:acp-agent-support:0.9.0-SNAPSHOT'
implementation 'com.agentclientprotocol:acp-websocket-jetty:0.9.0-SNAPSHOT'
testImplementation 'com.agentclientprotocol:acp-test:0.9.0-SNAPSHOT'
// build.gradle.kts
repositories {
    maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") }
}

implementation("com.agentclientprotocol:acp-core:0.9.0-SNAPSHOT")

// Optional modules
implementation("com.agentclientprotocol:acp-agent-support:0.9.0-SNAPSHOT")
implementation("com.agentclientprotocol:acp-websocket-jetty:0.9.0-SNAPSHOT")
testImplementation("com.agentclientprotocol:acp-test:0.9.0-SNAPSHOT")

Three Agent API Styles

Quick Comparison

FeatureAnnotation-basedSyncAsync
Entry Point@AcpAgent classAcpAgent.sync()AcpAgent.async()
Handler StyleAnnotated methodsLambda callbacksLambda callbacks returning Mono
Return ValuesAuto-converted (StringPromptResponse)Direct protocol typesMono<ProtocolType>
BoilerplateLowestModerateModerate
Best ForMost applicationsSimple blocking handlersReactive applications
RuntimeAcpAgentSupportAcpSyncAgentAcpAsyncAgent
All three produce identical protocol behavior and support the same capabilities.

When to Use Each

  • Annotation-based — default choice. Least boilerplate, auto-converts return types, supports interceptors and custom argument resolvers.
  • Sync — when you want explicit control over every handler without annotations. Blocking void methods for sending updates.
  • Async — when your agent needs non-blocking I/O. Uses Project Reactor Mono for composable async chains.

Client API

AcpClient — Factory

MethodReturn TypeDescription
sync(transport)AcpSyncClient.BuilderCreate blocking client builder
async(transport)AcpAsyncClient.BuilderCreate reactive client builder

AcpSyncClient — Blocking Client

MethodReturn TypeDescription
initialize()InitializeResponseProtocol handshake with defaults
initialize(request)InitializeResponseHandshake with custom capabilities
newSession(request)NewSessionResponseCreate a new session
loadSession(request)LoadSessionResponseResume an existing session
prompt(request)PromptResponseSend prompt, block until response
cancel(notification)voidCancel current prompt (fire-and-forget)
getAgentCapabilities()NegotiatedCapabilitiesCapabilities reported by agent
close()voidClose connection

Builder Configuration

AcpSyncClient client = AcpClient.sync(transport)
    .sessionUpdateConsumer(notification -> {
        // Handle streaming updates during prompt()
    })
    .readTextFileHandler(req -> {
        // Agent requests a file read
        return new ReadTextFileResponse(Files.readString(Path.of(req.path())));
    })
    .writeTextFileHandler(req -> {
        // Agent requests a file write
        Files.writeString(Path.of(req.path()), req.content());
        return new WriteTextFileResponse();
    })
    .requestPermissionHandler(req -> {
        // Agent requests permission
        return new RequestPermissionResponse(req.options().getFirst().id());
    })
    .build();

Example — Complete client lifecycle

This launches Gemini CLI as an ACP agent subprocess and sends it a prompt. AgentParameters builds the command line; StdioAcpClientTransport spawns the process and handles JSON-RPC framing over stdin/stdout.
// Launch "gemini --experimental-acp" as a subprocess
var params = AgentParameters.builder("gemini")
    .arg("--experimental-acp")
    .build();

var transport = new StdioAcpClientTransport(params);
AcpSyncClient client = AcpClient.sync(transport)
    .sessionUpdateConsumer(notification -> {
        var update = notification.update();
        if (update instanceof AgentMessageChunk msg) {
            System.out.print(((TextContent) msg.content()).text());
        }
    })
    .build();

client.initialize();
var session = client.newSession(new NewSessionRequest("/workspace", List.of()));

var response = client.prompt(new PromptRequest(
    session.sessionId(),
    List.of(new TextContent("Hello, world!"))
));

System.out.println("Stop reason: " + response.stopReason());
client.close();

Agent API — Annotation-Based

The acp-agent-support module provides a declarative programming model using annotations.

Annotations

Class-Level

AnnotationDescription
@AcpAgentMarks a class as an ACP agent. Optional name and version attributes.

Handler Methods

AnnotationJSON-RPC MethodDescription
@InitializeinitializeProtocol initialization and capability negotiation
@NewSessionsession/newCreates a new agent session
@LoadSessionsession/loadLoads an existing session by ID
@Promptsession/promptHandles user prompts
@SetSessionModesession/set_modeChanges operational mode
@SetSessionModelsession/set_modelChanges the AI model
@Cancelsession/cancelCancellation notification (fire-and-forget)

Parameter Annotations

AnnotationDescription
@SessionIdInjects the current session ID as String
@SessionStateInjects session-specific state

Flexible Method Signatures

Handler methods support flexible parameter resolution:
// Minimal
@Initialize
InitializeResponse init() { return InitializeResponse.ok(); }

// With request
@Prompt
PromptResponse answer(PromptRequest req) { ... }

// With context
@Prompt
PromptResponse answer(PromptRequest req, SyncPromptContext ctx) { ... }

// Auto-converted return types
@Prompt
String simpleAnswer(PromptRequest req) { ... }  // → PromptResponse.text(value)

@Prompt
void streaming(PromptRequest req, SyncPromptContext ctx) { ... }  // → endTurn()

Return Value Handling

Return TypeConversion
Protocol response typePassed through directly
StringConverted to PromptResponse.text(value)
voidConverted to PromptResponse.endTurn()
Mono<PromptResponse>Unwrapped and returned

SyncPromptContext

Available in @Prompt handlers. Provides blocking methods for agent-client interaction:
@Prompt
PromptResponse handle(PromptRequest req, SyncPromptContext ctx) {
    // Session info
    String sessionId = ctx.getSessionId();
    NegotiatedCapabilities caps = ctx.getClientCapabilities();

    // Messages and thoughts
    ctx.sendMessage("Working on it...");
    ctx.sendThought("Let me analyze this...");

    // File operations (requires client capabilities)
    String content = ctx.readFile("/path/to/file.txt");
    ctx.writeFile("/path/to/output.txt", "content");
    Optional<String> maybe = ctx.tryReadFile("/path/to/file.txt");

    // Permissions
    boolean allowed = ctx.askPermission("Delete files in /tmp?");
    String choice = ctx.askChoice("Which format?", "JSON", "XML", "YAML");

    // Terminal execution (requires client capabilities)
    CommandResult result = ctx.execute("ls", "-la");

    return PromptResponse.endTurn();
}

AcpAgentSupport — Bootstrap

AcpAgentSupport.create(new MyAgent())
    .transport(StdioAcpAgentTransport.create())
    .requestTimeout(Duration.ofSeconds(60))   // Optional
    .interceptor(new LoggingInterceptor())     // Optional
    .argumentResolver(new UserResolver())      // Optional
    .returnValueHandler(new FutureHandler())   // Optional
    .run();  // Blocks until client disconnects

Interceptors

Cross-cutting concerns like logging, metrics, or error handling:
public class LoggingInterceptor implements AcpInterceptor {

    @Override
    public boolean preInvoke(AcpInvocationContext context) {
        log.info("Invoking: {}", context.getAcpMethod());
        return true;  // Continue processing
    }

    @Override
    public Object postInvoke(AcpInvocationContext context, Object result) {
        log.info("Result: {}", result);
        return result;
    }

    @Override
    public int getOrder() { return 0; }  // Lower values execute first
}

Example — Complete annotation-based agent

@AcpAgent(name = "code-assistant", version = "1.0.0")
class CodeAssistant {

    private final Map<String, List<String>> sessionHistory = new ConcurrentHashMap<>();

    @Initialize
    InitializeResponse init() { return InitializeResponse.ok(); }

    @NewSession
    NewSessionResponse newSession(NewSessionRequest req) {
        String sessionId = UUID.randomUUID().toString();
        sessionHistory.put(sessionId, new ArrayList<>());
        return new NewSessionResponse(sessionId, List.of(), List.of());
    }

    @Prompt
    PromptResponse prompt(PromptRequest req, SyncPromptContext ctx) {
        ctx.sendThought("Analyzing the code...");

        if (ctx.getClientCapabilities().readTextFile()) {
            ctx.sendMessage("I can access files if needed.");
        }

        ctx.sendMessage("Here's my analysis...");
        return PromptResponse.endTurn();
    }

    @Cancel
    void onCancel(CancelNotification notification, @SessionId String sessionId) {
        sessionHistory.remove(sessionId);
    }
}

Agent API — Sync (Builder)

Blocking handlers with plain return values. No annotations.

Builder Methods

MethodDescription
initializeHandler(handler)Handle initialize requests
newSessionHandler(handler)Handle session/new requests
loadSessionHandler(handler)Handle session/load requests
promptHandler(handler)Handle session/prompt requests
cancelHandler(handler)Handle session/cancel notifications

Example

AcpSyncAgent agent = AcpAgent.sync(transport)
    .initializeHandler(req -> InitializeResponse.ok())

    .newSessionHandler(req ->
        new NewSessionResponse(UUID.randomUUID().toString(), null, null))

    .promptHandler((req, context) -> {
        context.sendMessage("Hello!");
        return PromptResponse.endTurn();
    })
    .build();

agent.run();  // Blocks until client disconnects

Prompt Handler Context

The context parameter in promptHandler provides:
MethodDescription
getSessionId()Current session ID
sendMessage(text)Send AgentMessageChunk
sendThought(text)Send AgentThoughtChunk
sendUpdate(sessionId, update)Send any SessionUpdate
readFile(path, offset, limit)Read file from client
writeFile(path, content)Write file on client
requestPermission(request)Ask client for permission
getClientCapabilities()Check client capabilities

Agent API — Async (Builder)

Reactive handlers returning Mono. Uses Project Reactor.

Example

AcpAsyncAgent agent = AcpAgent.async(transport)
    .initializeHandler(req ->
        Mono.just(InitializeResponse.ok()))

    .newSessionHandler(req ->
        Mono.just(new NewSessionResponse(
            UUID.randomUUID().toString(), null, null)))

    .promptHandler((req, context) ->
        context.sendMessage("Hello!")
            .then(Mono.just(PromptResponse.endTurn())))
    .build();

agent.start().then(agent.awaitTermination()).block();
The async context’s sendMessage(), sendUpdate(), etc. return Mono<Void>, composable with .then() and .flatMap().

Convenience Methods vs Full API

The SDK provides convenience methods that cover the most common operations. Use these by default — they produce cleaner code and handle the protocol details for you.

When convenience methods are enough (~80% of cases)

// Response factories
InitializeResponse.ok()                        // default capabilities
InitializeResponse.ok(customCapabilities)      // custom capabilities
PromptResponse.endTurn()                       // stop reason END_TURN
PromptResponse.text("response")                // message + endTurn in one call

// Sending updates (on SyncPromptContext or async equivalent)
context.sendMessage("Hello");                  // AgentMessageChunk with TextContent
context.sendThought("Analyzing...");           // AgentThoughtChunk with TextContent

// File operations
String content = context.readFile("pom.xml");  // read with defaults
context.writeFile("output.txt", "content");    // write file

// Terminal
CommandResult result = context.execute("ls", "-la");

// Permissions
boolean ok = context.askPermission("Delete temp files?");
String choice = context.askChoice("Format?", "JSON", "XML", "YAML");

When to use the full API (~20% of cases)

Drop to the full API when you need control that convenience methods don’t expose:
// Custom AgentCapabilities with specific MCP and prompt settings
var caps = new AgentCapabilities(
    true,                                          // loadSession
    new McpCapabilities(true, true),               // HTTP + SSE
    new PromptCapabilities(true, false, true)       // imageContent, audioContent, embeddedContext
);
return InitializeResponse.ok(caps);

// Send non-text update types (Plan, ToolCall, AvailableCommandsUpdate, etc.)
context.sendUpdate(sessionId, new Plan("plan", List.of(
    new PlanStep("Analyze code", PlanStepStatus.IN_PROGRESS),
    new PlanStep("Generate tests", PlanStepStatus.PENDING)
)));

context.sendUpdate(sessionId, new ToolCall("tool_call",
    "search", "code-search", ToolCallStatus.IN_PROGRESS, null));

// Read file with offset and line limit
var response = context.readTextFile(
    new ReadTextFileRequest(sessionId, "large-file.txt", 100, 50));

// Request permission with custom options
var permResponse = context.requestPermission(new RequestPermissionRequest(
    sessionId, "Run deployment script?", List.of(
        new PermissionOption("allow-once", "Allow once", PermissionOptionKind.ALLOW_ONCE),
        new PermissionOption("always", "Always allow", PermissionOptionKind.ALLOW_ALWAYS),
        new PermissionOption("reject", "Reject", PermissionOptionKind.REJECT_ONCE)
    )));
The convenience methods are wrappers around the full API — they call the same underlying protocol methods. You can mix and match freely within a single handler.

Protocol Types

All protocol types are defined in AcpSchema as Java records.

Request/Response Types

TypeFields
InitializeRequestprotocolVersion, clientCapabilities
InitializeResponseprotocolVersion, agentCapabilities, sessionIds
NewSessionRequestcwd, mcpServers
NewSessionResponsesessionId, restoredSessionState, contextDocuments
LoadSessionRequestsessionId, cwd, mcpServers
LoadSessionResponserestoredSessionState, contextDocuments
PromptRequestsessionId, prompt (list of Content)
PromptResponsestopReason
CancelNotificationsessionId

Content Types

TypeDescription
TextContentText content with text field
ImageContentImage content (base64 or URL)

Session Update Types

TypeDescription
AgentMessageChunkIncremental response text
AgentThoughtChunkAgent thinking process
ToolCallTool execution start
ToolCallUpdateNotificationTool progress update
PlanAgent’s planned steps
AvailableCommandsUpdateAdvertised slash commands
CurrentModeUpdateAgent mode change

Stop Reasons

ValueDescription
END_TURNAgent finished responding
MAX_TOKENSToken limit reached
REFUSALAgent refused the request
CANCELLEDPrompt was cancelled

Convenience Methods

// Static factory methods
InitializeResponse.ok()
PromptResponse.endTurn()
PromptResponse.text("response")

Capabilities

Client Capabilities

Advertised during initialize:
client.initialize(new InitializeRequest(1,
    new ClientCapabilities(
        new FileSystemCapability(true, true),  // read, write
        true  // terminalExecution
    )));

NegotiatedCapabilities

Check capabilities before using them:
NegotiatedCapabilities caps = context.getClientCapabilities();
if (caps.supportsReadTextFile()) {
    String content = context.readFile("file.txt");
}
if (caps.supportsWriteTextFile()) {
    context.writeFile("output.txt", "content");
}
Or use require methods that throw AcpCapabilityException if unsupported:
caps.requireWriteTextFile();
context.writeFile("output.txt", "content");

Transports

TransportClient ClassAgent ClassModule
StdioStdioAcpClientTransportStdioAcpAgentTransportacp-core
WebSocketWebSocketAcpClientTransportWebSocketAcpAgentTransportacp-core / acp-websocket-jetty
In-Memoryvia InMemoryTransportPairvia InMemoryTransportPairacp-test

Stdio Transport

The default transport. The client launches the agent as a subprocess and communicates via JSON-RPC over stdin/stdout. This is the same mechanism Zed, JetBrains, and VS Code use to talk to agents. Client sideAgentParameters specifies the command to launch. Any executable that speaks ACP over stdin/stdout works (Gemini CLI, your own agent JAR, etc.):
var params = AgentParameters.builder("gemini")
    .arg("--experimental-acp")
    .build();
var transport = new StdioAcpClientTransport(params);
Agent side — reads JSON-RPC from stdin, writes responses to stdout. The agent doesn’t need to know what launched it:
var transport = new StdioAcpAgentTransport();

WebSocket Transport

For network-based communication. Client (JDK-native, no extra dependencies):
var transport = new WebSocketAcpClientTransport(
    URI.create("ws://localhost:8080/acp"),
    McpJsonMapper.getDefault()
);
Agent (requires acp-websocket-jetty):
var transport = new WebSocketAcpAgentTransport(
    8080, "/acp", McpJsonMapper.getDefault()
);

In-Memory Transport

For testing. No subprocess or network I/O.
var pair = InMemoryTransportPair.create();
// pair.clientTransport() — for client
// pair.agentTransport() — for agent
// pair.closeGracefully() — cleanup

Errors

Exception Hierarchy

ExceptionDescription
AcpProtocolExceptionJSON-RPC protocol error with code and message
AcpCapabilityExceptionTried to use an unsupported capability
AcpConnectionExceptionTransport-level connection failure

Error Codes

try {
    client.prompt(request);
} catch (AcpProtocolException e) {
    if (e.isConcurrentPrompt()) {
        // Another prompt is already in progress
    } else if (e.isMethodNotFound()) {
        // Agent doesn't support this method
    }
    System.err.println("Error " + e.getCode() + ": " + e.getMessage());
} catch (AcpCapabilityException e) {
    System.err.println("Not supported: " + e.getCapability());
}

Agent-Side Error Handling

Throw AcpProtocolException from handlers to send structured errors to clients:
.promptHandler((req, context) -> {
    if (req.prompt().isEmpty()) {
        throw new AcpProtocolException(
            AcpErrorCodes.INVALID_PARAMS, "Empty prompt");
    }
    // ...
})

Test Utilities

The acp-test module provides utilities for testing without subprocesses.

InMemoryTransportPair

var pair = InMemoryTransportPair.create();

// Wire up agent
AcpAsyncAgent agent = AcpAgent.async(pair.agentTransport())
    .initializeHandler(req -> Mono.just(InitializeResponse.ok()))
    .newSessionHandler(req -> Mono.just(
        new NewSessionResponse(UUID.randomUUID().toString(), null, null)))
    .promptHandler((req, context) ->
        context.sendMessage("response")
            .then(Mono.just(PromptResponse.endTurn())))
    .build();

agent.start().subscribe();

// Wire up client
AcpSyncClient client = AcpClient.sync(pair.clientTransport()).build();
client.initialize();
// ... test ...

pair.closeGracefully().block();

Packages

PackageDescription
com.agentclientprotocol.sdk.specProtocol types (AcpSchema.*)
com.agentclientprotocol.sdk.clientClient SDK (AcpClient, AcpAsyncClient, AcpSyncClient)
com.agentclientprotocol.sdk.agentAgent SDK (AcpAgent, AcpAsyncAgent, AcpSyncAgent)
com.agentclientprotocol.sdk.agent.supportAnnotation-based agent runtime (AcpAgentSupport)
com.agentclientprotocol.sdk.annotationAgent annotations (@AcpAgent, @Prompt, etc.)
com.agentclientprotocol.sdk.capabilitiesCapability negotiation (NegotiatedCapabilities)
com.agentclientprotocol.sdk.errorExceptions (AcpProtocolException, AcpCapabilityException)
com.agentclientprotocol.sdk.testTest utilities (InMemoryTransportPair)

Maven Artifacts

ArtifactDescription
acp-coreClient and Agent SDKs, stdio and WebSocket client transports
acp-annotations@AcpAgent, @Prompt, and other annotations
acp-agent-supportAnnotation-based agent runtime (includes acp-annotations + acp-core)
acp-testIn-memory transport and test utilities
acp-websocket-jettyJetty-based WebSocket server transport for agents

See Also