acp-quarkus is a required (non-preview) Quarkus extension for the ACP Java SDK as of 0.80.0, built
against Quarkus 3.40.1. With it, all three JVM framework integrations the SDK ships are the same
shape: annotate one class, let the framework serve it. JVM mode only for 0.80.0; there’s no native
image yet.
Quick Start
jackson-bom import, Quarkus’s own managed Jackson (2.21.7, as of Quarkus 3.40.1)
applies instead; both lines are supported, and the extension’s own tests run on the Quarkus-managed
version. Reactor (3.8.7) is the same either way, and Netty is Quarkus’s own.
An agent, annotated
@AcpAgent alone makes the class a CDI bean, a singleton unless it declares its own scope. It’s
discovered at build time: a second @AcpAgent class fails the build, naming both. With none, the
application is a client only. Capabilities and agentInfo are derived from the annotations the same
way as everywhere else; see Clients and Agents in Java for the
full derivation and @Initialize-merge rules, which apply unchanged here.
Intercepted beans work
So when an interceptor (@Transactional, a custom @Interceptor, or anything else CDI wraps the bean
in) applies to the agent, its advice still runs on every ACP call: discovery still finds @Prompt and
friends on the class they’re actually declared on, while invocation goes through the container’s
managed instance. No special wiring needed.
AcpInterceptor, ArgumentResolver, and ReturnValueHandler beans
Beans of these three types are picked up and added to the agent automatically, the CDI-native way to
extend the handler pipeline.
Mutiny return types
A handler may return a MutinyUni of what it could return directly: a Uni<String> from @Prompt
sends the text and ends the turn, a Uni<Void> just ends it. The handler’s thread waits for the
item, the same as it would for a Mono.
@Prompt may also return a Multi, which no other handler method may: streaming String (sent as a
message chunk), SessionUpdate (sent as that update), or ContentBlock (wrapped in a message chunk)
items, each as it’s emitted, ending the turn (end_turn) when the stream completes. Cancelling the
prompt cancels the Multi’s subscription and ends the turn cancelled; a failed stream fails the
prompt.
Over stdio (the default)
Standard output carries the protocol, so a stdio build sets these defaults, your own settings win:quarkus.log.console.stderr=true, quarkus.banner.enabled=false, quarkus.http.host-enabled=false.
Never print to System.out. The application exits when the client closes standard input
(quarkus.acp.agent.shutdown-on-transport-end=false keeps it running).
Over HTTP and WebSocket: Quarkus’s own server
quarkus.http.port (under
quarkus.http.root-path): Streamable HTTP, SSE and the WebSocket upgrade, all on one Vert.x route of
the Quarkus router, with no servlet container and no second server to run or port to open; unlike
Micronaut and Spring Boot in their
non-web mode, which each open a standalone listener of their own. The route sits behind Quarkus’s own
authentication and permission checks, so HTTP security policies apply to /acp, the WebSocket
handshake included.
Each connection runs its own agent runtime over the one bean, so its handlers must be thread-safe,
same as the other two integrations. On shutdown the endpoint drains first, within
quarkus.acp.agent.transport.http.shutdown-timeout: each open SSE stream gets a closing comment and
completes, and each WebSocket closes with 1001, the drain waiting for the close frame to actually go
out before reporting that connection closed. A client that never answers holds the shutdown no longer
than the timeout, after which it’s logged at WARN and closed at once.
An HTTP build raises these Quarkus defaults to what the SDK’s listener admits (your own settings
win): quarkus.http.limits.max-body-size=16M, quarkus.http.websocket-server.max-message-size and
max-frame-size to 16 MiB, and quarkus.http.limits.max-concurrent-streams=1024 (each SSE stream is
one HTTP/2 stream). Raise them too if you raise max-post-body-size past 16 MiB.
Quarkus’s own quarkus.http.idle-timeout (30 minutes by default) also closes idle ACP WebSockets; the
shorter of it and quarkus.acp.agent.transport.http.web-socket-idle-timeout wins.
Native integration, not a bolt-on
The agent is served on Quarkus’s own HTTP server: one server, one port, one TLS setup, one lifecycle that starts and stops with the application, one place for metrics and logs, and Quarkus’s own threads. There’s nothing ACP-specific to configure alongside what the application already has.If you need it: securing /acp
Most ACP use (an IDE talking stdio, a handful of local sessions) needs no security configuration at
all. If you do expose the agent remotely, it’s served behind Quarkus’s own request handling, so
ordinary Quarkus HTTP security policies and @RolesAllowed apply to /acp the same as any other
endpoint, the WebSocket handshake included. Proven by the SDK’s own test: an unauthenticated request
gets 401 on HTTP and on the WebSocket handshake, an authenticated one gets 200/101, same as any other
secured endpoint. Restricting the ACP endpoint to a role, for example:
A client, from configuration
Client callbacks run on SDK threads, not on a Vert.x context. From a
Uni built over a client call,
hop back onto the Vert.x context with emitOn when you need it there.Configuration (quarkus.acp.*)
Build time (fixed when the application is built):
Run time:
“Only what a customizer registers handlers for” is enforced at startup: setting a capability
true
without its handler fails client creation, naming the property to turn off or the handler to add, for
example “fs.readTextFile needs readTextFileHandler… The capabilities come from
quarkus.acp.client.capabilities.read-text-file=true”. A customizer’s own sessionUpdateHandler
replaces the client’s DEBUG-logging one rather than running alongside it.spring.acp.*: the HTTP port is quarkus.http.port (not an
acp.*-namespaced property), and the listener’s max-concurrent-streams-per-connection is
quarkus.http.limits.max-concurrent-streams, Quarkus’s own HTTP tuning, not the SDK’s.
AcpClientCustomizer and AcpTransportType (used above by quarkus.acp.client.transport.type) are
not Quarkus-specific types: both live in com.agentclientprotocol.sdk.integration, the
framework-neutral module Spring Boot and Micronaut build on too; see
Concepts: the framework-neutral layer.
The client (and its transport) is closed exactly once, bounded by its request timeout plus 10 seconds,
by that same shared layer.
Where handlers run
By default (quarkus.acp.handler-executor=managed), handler methods run on the ManagedExecutor
(Quarkus’ own worker pool, where blocking work belongs, never the Vert.x event loop), with the
application’s contexts propagated, instead of a second pool of the SDK’s (see
Concepts: Where handlers run).
Set quarkus.acp.handler-executor=virtual to opt into Quarkus’ virtual-thread executor on JDK 21 and
later (it falls back to managed on earlier JDKs). Virtual threads keep the Vert.x context of the
submitting thread, but propagate no MicroProfile Context Propagation context: a handler gets no CDI
request context of its own unless the method carries @ActivateRequestContext, and no security
identity, transaction, or tracing context captured from the submitting thread.
Logging and sensitive data
See Transports: Logging and sensitive data for what the SDK logs at each level. Without a session-update handler of your own, session updates are logged at DEBUG, content included. A stdio agent’s standard error is logged at INFO, unfiltered, oncom.agentclientprotocol.sdk.client.transport.agent-stderr; silence it with
quarkus.log.category."com.agentclientprotocol.sdk.client.transport.agent-stderr".level=WARN.
Related
Spring Boot Starter
The equivalent integration for Spring Boot applications
Micronaut Integration
The equivalent integration for Micronaut applications
Clients and Agents in Java
The annotation model this module builds on
Transports
Where ACP listens in every deployment shape, Quarkus included