The framework-neutral half of the ACP Java SDK's framework integrations. The Spring Boot
autoconfiguration, the Micronaut integration and the Quarkus extension are each a thin layer over
this module: they bind their own configuration, find the @AcpAgent bean their own way, and call
the hosts here from their own lifecycle hooks. Everything that does not depend on a framework lives
here, once.
Use it to integrate ACP with another container. An application on Spring Boot, Micronaut or
Quarkus does not use it directly, except for AcpClientCustomizer.
<dependency>
<groupId>com.agentclientprotocol</groupId>
<artifactId>acp-integration</artifactId>
</dependency>
<!-- For an agent served over Streamable HTTP or WebSocket -->
<dependency>
<groupId>com.agentclientprotocol</groupId>
<artifactId>acp-streamable-http-jetty</artifactId>
</dependency>The module depends on acp-core and acp-agent-support, with acp-streamable-http-jetty
optional, and on no framework; an ArchUnit test enforces both, and that the SDK below does not
depend on it. Package com.agentclientprotocol.sdk.integration, null-marked.
AcpAgentSettings and AcpClientSettings are immutable records with builders. A value left unset
keeps the SDK's default, so no default is decided twice. A framework binds its configuration onto
the builders (Quarkus @ConfigMapping, Spring and Micronaut @ConfigurationProperties), or reads
plain key/value configuration with from(SettingsSource, prefix):
| Agent key | Default |
|---|---|
enabled |
true |
request-timeout, cancel-grace-period, max-prompt-duration |
SDK defaults |
shutdown-on-transport-end |
true: stop the application when stdio input ends |
transport.type |
stdio; http (or websocket, the same) for Streamable HTTP and WebSocket. The SDK's listener takes WebSocket upgrades on the endpoint's path; the servlet in a framework's Servlet container does not, so there websocket serves HTTP/SSE only, unless the framework routes the upgrades itself (Quarkus does) |
transport.http.path |
/acp |
transport.http.max-post-body-size, keep-alive-interval, mailbox-capacity, max-pending-sse-events, max-web-socket-pending-frames, max-provisional-sessions, shutdown-timeout, web-socket-idle-timeout, initialize-timeout |
SDK defaults |
transport.http.listener.port |
8080, the SDK listener only; 0 for an ephemeral port |
transport.http.listener.max-concurrent-streams-per-connection |
SDK default |
| Client key | Default |
|---|---|
request-timeout |
SDK default |
prompt-timeout |
none |
transport.type |
inferred (below) |
transport.stdio.command, args, env |
the agent process to start |
transport.websocket.uri, connect-timeout |
10s |
transport.http.uri |
|
capabilities.read-text-file, write-text-file, terminal, elicitation-form, elicitation-url, boolean-config-options |
false |
Advertise a capability only together with the handler for it, registered through an
AcpClientCustomizer.
AcpClientTransports.create(settings, prefix) applies one rule in every framework: an explicit
transport.type wins and fails without its command or URI; otherwise exactly one of
stdio.command, websocket.uri and http.uri selects the transport, and more than one fails,
naming them; with none there is no client. Error messages name the framework's own keys.
AcpAgentTransports.stdio() is the stdio agent transport. AcpListeners creates the endpoints
of the optional acp-streamable-http-jetty module: listener(settings, factory) for the SDK's own
listener (HTTP, cleartext HTTP/2 and WebSocket on one path) and servlet(settings, factory) for a
framework's Servlet container; isListenerAvailable() says whether the listener and its Jetty
server are on the classpath. The module's types appear in AcpListeners, AcpListenerHost,
AcpServletHost and AcpAgentSettings.toOptions. A framework without the module still loads
these classes, AcpAgentSettings included, because the JVM resolves those types only when code
that uses them runs: call them only when the agent is served over HTTP, and call
isListenerAvailable() before listener to report a missing module in the framework's own
words.
List<AgentCandidate<?>> candidates = ...; // the container's @AcpAgent beans: user class, instance supplier
AcpAgentDiscovery.requireSingle(candidates, "my.acp.agent.enabled").ifPresent(agent -> {
AcpAgentSupport.Builder builder = AcpAgents.builder(agent, settings, interceptors, resolvers, handlers);
AcpHost host = settings.servesHttp()
? new AcpListenerHost(AcpListeners.listener(settings, builder.buildFactory()))
: new AcpAgentHost(builder.transport(AcpAgentTransports.stdio()).build(),
settings.shutdownOnTransportEnd() ? container::close : () -> { });
host.start(); // from the container's start hook
// host.stop(Duration.ofSeconds(30)) // from its stop hook, or a JVM shutdown hook
});AcpAgentDiscovery.requireSingle: none is a client-only application, more than one an error naming them. A candidate carries the user class from the container's metadata, never a proxy class, and a supplier of the bean, which may be a proxy and keeps its advice. A build-time form takes class names (a Jandex index) and loads nothing.AcpAgents.builder: handlers discovered on the user class, the settings' timeouts, then the interceptors, argument resolvers and return value handlers in order.AcpHost:start(),stopGracefully(),stop(Duration)(safe from a shutdown hook, also whilestart()runs),termination(),port(), andholdJvmUntilTermination()for an application with nothing else keeping the JVM up. Nothing blocks exceptstop, so a framework whosemainreturns can host an agent too.AcpAgentHost: one agent on one transport, on the SDK's own lifecycle (AcpAgentSupport'sstart()andclose(), its agent'sawaitTermination()). When the transport ends by itself, theonTransportEndaction given at construction runs once, on a thread of the host's own, never when the host stopped the agent itself.AcpListenerHost: the SDK listener, one agent per connection; it ends only when stopped.AcpServletHost.closeBeforeShutdown: closes the servlet's connections before the container's graceful shutdown, which would otherwise wait for every open SSE response.
AcpClients.async(transport, settings, customizers) builds the one async client: capabilities,
request and prompt timeouts, a session-update handler that logs each update at DEBUG, its content
included, then the
AcpClientCustomizers in order. Building the client connects the transport (for stdio, it starts
the agent process); a stdio command that cannot be started, or a transport already connected,
fails the build. The ACP handshake waits for the application's initialize().
AcpClients.sync(async) is the sync facade over that same client
(one session on one connection). AcpClientHost closes it once, gracefully, then at once after
its timeout (settings.closeTimeout()); the framework must not close the client or its transport
again through an inferred destroy method.
Logging: DEBUG and TRACE on com.agentclientprotocol.sdk log message payloads in full, session
updates included; do not enable them where logs are shipped. A stdio agent's standard error is
logged at INFO, unfiltered, on com.agentclientprotocol.sdk.client.transport.agent-stderr, and can
carry sensitive data: set that logger to WARN or OFF, or register a handler of your own with
StdioAcpClientTransport.setStdErrorHandler.
The Mutiny Uni and Multi return types stay in acp-quarkus; the CompletionStage and
Publisher ones are in acp-agent-support. Planned after 0.80.0: a container-neutral WebSocket
connection API, so Vert.x, Netty and other servers route WebSocket upgrades to the SDK without
Jetty, and a Jetty-free acp-streamable-http-servlet module.