Repository navigation
ACP Java SDK 0.80.0
The biggest release of the ACP Java SDK so far. It covers every stable ACP v1 method on both sides. It adds first-class Spring Boot, Quarkus and Micronaut support. Its annotation model now drives what an agent tells the protocol about itself. Every change is proven against the TypeScript, Rust, Python and Kotlin SDKs.
This release breaks the API in places, as a pre-1.0 SDK may. Every break has a migration in CHANGELOG.md. Start with Upgrading from 0.18.0 below.
Highlights
Spring Boot, Quarkus and Micronaut
Each framework has its own idiomatic integration. All three build on one framework-neutral module, acp-integration, for configuration, discovery and lifecycle.
| Framework | Artifact | Config prefix | HTTP / WebSocket |
|---|---|---|---|
| Spring Boot 4 (MVC) | acp-spring-boot-starter + acp-http-servlet |
spring.acp.* |
on the app's own server.port (Tomcat or Jetty) |
| Spring Boot 4 (WebFlux) | acp-spring-boot-starter + acp-http-webflux |
spring.acp.* |
on the app's own server.port (Reactor Netty by default) |
| Spring Boot 4 (no web server) | acp-spring-boot-starter + acp-streamable-http-jetty |
spring.acp.* |
the SDK's embedded listener |
| Quarkus 3 | acp-quarkus |
quarkus.acp.* |
Quarkus's own HTTP server and router (no Jetty, no Undertow) |
| Micronaut 4 | acp-micronaut + acp-streamable-http-jetty (for HTTP / WebSocket) |
acp.* |
the SDK's embedded listener, on its own port (interim; see Known issues) |
All three integrations support the following:
- An
@AcpAgentbean is discovered, including when it is proxied or intercepted. - stdio, Streamable HTTP and WebSocket transports.
- A client bean built from configuration, plus customizers.
- Handlers run on the framework's own executor (virtual threads where available).
- A clean exit when stdin closes, and a graceful shutdown.
- On Spring (MVC and WebFlux) and Quarkus,
/acpis served by the application's own server, so the framework's security, TLS, metrics and tracing apply to it. Tests prove that Spring Security and Quarkus HTTP auth policies guard/acp, over HTTP and on the WebSocket handshake, and that the authenticated principal reaches the ACP endpoint (AcpHttpExchange.principal()).
Quarkus handlers can also return Mutiny Uni and Multi, and cancelling a prompt cancels them.
The Spring Boot integration was previously org.springaicommunity:acp-spring-boot-starter. It now ships with the SDK under com.agentclientprotocol.
The annotation model is the API
- Advertises what you implement. The initialize response is derived from your handlers: load, list, resume, close, delete, logout and providers. Prompt and MCP capabilities, auth methods and
agentInfocome from@AcpAgentand@Prompt. An explicit@Initializeis merged on top of the derived values. - New annotations and parameters.
@Authenticateis new. Any handler can take its connection'sNegotiatedCapabilitiesand agent, and@SetSessionConfigOptiongets typed@ConfigIdand@ConfigValueparameters. - Prompt handlers. They can check
isCancelled(), and they can returnCompletionStageorPublisheras well as plain values andMono. - Mistakes fail at startup, with the class, method and fix named. This covers unresolvable parameters, unsupported return types, duplicate handlers and annotations in the wrong place.
- Parity with the builder API, enforced by a test.
Protocol
- Every stable ACP v1 method and type, on both client and agent. That includes:
- session config options (select and boolean, grouped selects), which replace
session/set_model; - elicitation (form and URL);
- terminal authentication and logout;
- session list, resume, close and delete;
_-prefixed extension methods;$/cancel_request.
- session config options (select and boolean, grouped selects), which replace
- Cancellation follows the spec exactly. After
session/cancelthe agent answerscancelled, whatever the handler returns or throws. Updates sent after the answer are dropped. A configurable grace period and an optional maximum prompt duration bound the turn. - Forward compatible. Unknown updates, content, options and enum values from a newer peer are kept and passed on.
- Ordering. A client's response, and any request from the agent, arrives only after the updates sent before it have been handled.
- Clients guard themselves.
- Calls before
initialize(), and calls the agent didn't advertise, fail locally. - A capability advertised without its handler fails at
build(). - Prompts are no longer cut off by the request timeout;
promptTimeoutis optional and unset by default.
- Calls before
- One error model.
- Handlers throw
AcpProtocolException. - Callers catch
AcpError, now anAcpException, for every failed request. - A lost connection fails with
AcpConnectionException. - Unexpected handler exceptions answer "Internal error" without leaking their message to the peer.
- Handlers throw
Transports and hosting
-
One protocol core, thin hosts. Every Streamable HTTP and WebSocket rule lives in one framework-neutral core (
acp-http-core). Each host only adapts I/O, through a small host contract (@UnstableAcpApiin 0.80.0). Hosts in 0.80.0:- a servlet with Jakarta WebSocket 2.1, which upgrades on its own path in any Servlet 6 container (
acp-http-servlet); - Spring WebFlux (
acp-http-webflux); - Quarkus on its Vert.x router;
- the embedded Jetty listener for plain Java (
acp-streamable-http-jetty).
One shared transport test kit runs against all nine host configurations. Plain
http://on the embedded listener gets HTTP/2 (h2c). - a servlet with Jakarta WebSocket 2.1, which upgrades on its own path in any Servlet 6 container (
-
Streaming defaults every host gets:
- SSE carries
X-Accel-Buffering: no, so nginx doesn't buffer it, and isn't cut by container async timeouts; - WebSocket sends are serialized;
- shutdown drains open streams promptly, with a final event and close 1001, within a bounded timeout.
- SSE carries
-
An ACP session is a long-lived conversation, not a web request. Every WebSocket host closes a connection that stays idle past
web-socket-idle-timeout(30 minutes by default, close 1001), and one that never sendsinitializewithininitialize-timeout(30 s by default, close 1008). Set them withspring.acp.agent.transport.http.web-socket-idle-timeout/.initialize-timeout, the same keys underacp.(Micronaut) andquarkus.acp., or the transport options builder. -
Shutdown is clean:
- a WebSocket that opens while the server shuts down no longer leaves its agent running;
- WebSocket clients get the going-away close (1001) when a Jetty-hosted endpoint stops;
- a client that never answers the close holds shutdown no longer than its timeout.
-
stdio: end of input drains every answer, and a graceful client close lets the agent exit with status 0.
-
Threads: the network transports run on the application's executor. On JDK 21+, plain Java and Micronaut use virtual threads; Spring follows
spring.threads.virtual.enabled; Quarkus uses its managed executor unlessquarkus.acp.handler-executor=virtual. -
Close: idempotent on every transport, with a bounded shutdown.
Security
- The embedded listener binds to loopback by default (127.0.0.1, and ::1 where available). Exposing it is an explicit opt-in (
…transport.http.listener.host). Framework-hosted ACP keeps the framework's own bind setting. - Browser origins are checked on HTTP and on the WebSocket handshake, on every host. Requests with no
Origin, or a loopback origin, are allowed. Others get 403 unless listed inallowed-origins;*disables the check. This defends against DNS rebinding. - Logs don't carry payloads above DEBUG.
- A malformed message is reported without its content.
- The WebSocket client logs its endpoint without query or user info.
- A peer's error details are logged only at DEBUG.
- Agent stderr is logged at INFO on its own logger (
…client.transport.agent-stderr), and the README says how to silence it. - DEBUG and TRACE log message content, and the docs say not to enable them where logs are shipped.
- Handler exceptions answer "Internal error" without leaking their message.
Quality
These checks are enforced in CI, with no baselines:
- NullAway (JSpecify, every package)
- Error Prone
- ArchUnit (no cycles, framework-neutral core)
- PMD and CPD
- SpotBugs, including the concurrency patterns
- JaCoCo coverage floors
- Lincheck model checks of the concurrency core
- PIT mutation testing on the protocol core (a dedicated CI job)
- an OSV dependency scan
A cross-SDK suite runs the Java client and agent against TypeScript, Rust, Python and Kotlin, over stdio, HTTP and WebSocket, plus Spring, Micronaut and Quarkus-hosted agents.
The public API is documented throughout, and the Javadoc examples are compile-checked.
Upgrading from 0.18.0
The most common changes are listed below. CHANGELOG.md has every break and its migration.
- Removed APIs:
session/set_modelis gone; offer amodelcategory config option instead.acp-websocket-jettyis removed;StreamableHttpAcpAgentTransportaccepts WebSocket upgrades.PromptResponse.text(...)is removed; return theStringfrom@Prompt, orsendMessagethenendTurn().
- Values and records:
- Several enums are now open value types. Compare them with
equals, notswitch. - The short session-update constructors no longer take the discriminator.
- Several enums are now open value types. Compare them with
- Client:
initialize(InitializeRequest)is removed; set capabilities andclientInfoon the builder.- Register each client and agent handler once; a second registration throws.
- The default request timeout is 60 s everywhere.
- Prompt context and handlers:
context.sendUpdate(u)is nowcontext.sendSessionUpdate(u);- the raw client calls (file system, permission, terminal, elicitation) moved behind
context.client(); sessionUpdateConsumer/defaultSessionUpdateConsumerare nowsessionUpdateHandler/defaultSessionUpdateHandler;- a client with handlers but no explicit
clientCapabilitiesnow advertises them.
- HTTP hosting:
- A servlet web app (Spring MVC included) depends on
acp-http-servlet, a WebFlux app onacp-http-webflux, and a plain-Java app keepsacp-streamable-http-jetty. - The embedded listener binds to loopback; set
…listener.hostto expose it. - Foreign browser origins get 403 unless listed in
allowed-origins. - WebSocket shutdown closes with 1001, not 1000.
- Quarkus serves
/acpunderquarkus.http.root-pathand no longer brings in Undertow. - Micronaut's listener keys are under
acp.agent.transport.http.listener.*.
- A servlet web app (Spring MVC included) depends on
- Spring Boot:
- New coordinates
com.agentclientprotocol:acp-spring-boot-starter. - Packages moved to
com.agentclientprotocol.sdk.spring.boot.autoconfigure. - Some properties moved; see the CHANGELOG and the module README.
- New coordinates
Requirements
- Java 17 or later.
- Jackson 2.18.1 or later, or Jackson 3.0.0 or later. The version is checked at startup.
- Spring Boot 4.1, Quarkus 3.40, or Micronaut 4.10, for the framework modules.
Known issues
- Spring WebFlux on Jetty: Jetty's connection idle timeout, 30 s by default, closes an ACP WebSocket after 30 s without traffic. Set
server.jetty.connection-idle-timeout(e.g.30m), or use Reactor Netty, the default for WebFlux. A fix is planned for 0.80.1. - Micronaut serves ACP on the SDK's embedded listener, on its own port, so Micronaut's own security does not apply to it. This is an interim measure until the Netty host in 0.81.0.
- An oversized request body may occasionally surface as a connection error rather than HTTP 413 on Tomcat (reactive) and Jetty hosts. The request is still refused; only the error the client sees differs. Bodies within the limit (16 MiB by default) are unaffected.
- Outbound queues on the stdio transports and the WebSocket client are unbounded: a peer that keeps sending requests without reading responses grows the sender's memory, about one response per request. Server-side HTTP and WebSocket hosts are bounded. Bounded outbound queues are planned for 0.81.0.
Full Changelog: v0.18.0...v0.80.0