acp-micronaut is a required (non-preview) module of the ACP Java SDK as of 0.80.0: Micronaut 4
integration that serves your @AcpAgent bean and builds an ACP client from configuration, the same
shape as the Spring Boot integration but wired through Micronaut’s
compile-time bean definitions instead of Spring’s runtime ones.
Quick Start
micronaut-inject-java) must be on your compiler’s annotation
processor path, as in any Micronaut application; Java 17+.
An agent, annotated
Annotate the agent class@AcpAgent, and make it a bean with @Singleton (any bean-defining
annotation works). It’s an ordinary Micronaut bean, so other beans inject into it normally:
@Prompt, @NewSession,
@SessionId, NegotiatedCapabilities, AcpSyncAgent/AcpAsyncAgent, and so on. There’s no
Micronaut-specific builder alternative; this module is annotation-only.
Discovery is compile-time, not a classpath scan
AcpAgentRuntime reads the @AcpAgent bean from the bean definitions Micronaut’s annotation
processor wrote at compile time, not by scanning the classpath at startup. Exactly one such bean is
allowed per application; with more than one, startup fails and names them all
(IllegalStateException, listing every bean’s class name). An application with none, such as a
client-only one, simply starts no agent.
agentInfo and capabilities come from the annotations
The initialize answer is derived the same way as plain Java and Spring: agentInfo from
@AcpAgent’s name/version, and a capability for each handler method the class declares. One bean
instance serves every session, and (over HTTP) every connection, so keep per-session state in a map
keyed by session id rather than on the bean itself.
Micronaut AOP works
Discovery finds the agent by its declared bean type, which carries the handler annotations; invocation goes through Micronaut’s owncontext.getBean(...), which is the actual runtime
instance, a generated AOP subclass when @Around advice applies to the bean. So an
@Around-intercepted agent is discovered correctly off its user class (where the @Prompt and
friends live), and calling it at runtime still goes through the generated subclass, so the advice
actually runs. No special wiring needed beyond the ordinary Micronaut AOP annotations.
Return types: Mono, Flux, CompletionStage, or any Publisher
A handler may return its response type directly, a Mono, a CompletionStage, or any single-value
Reactive Streams Publisher: a Flux, for instance, if that’s what a Micronaut data or HTTP client
call already returns. Its first element (or completion value) becomes the response; an empty one
means no response. The handler still runs, and is waited on, synchronously under the hood (the
runtime serves handlers through the sync agent), so this is a convenience for signature compatibility
with reactive Micronaut code, not a path to true non-blocking handling.
Lifecycle
- The agent starts with the application context (
StartupEvent) and closes gracefully with it. - Over stdio, the application stays up while the transport runs; when the client closes the
agent’s input and every answer has been written, the context closes and the process exits
0(acp.agent.shutdown-on-transport-end). - Over HTTP, the listener runs until the application stops.
- On SIGTERM, the agent closes gracefully. An application with no embedded server has no shutdown hook of Micronaut’s own, so the runtime registers one.
- With
micronaut.lifecycle.graceful-shutdown.enabled=true, the HTTP listener drains through Micronaut’s graceful shutdown first.
AcpAgentTransport replaces stdio, for example acp-test’s in-memory
transport in a test.
Agent Settings (acp.agent.*)
Only ACP’s own
web-socket-idle-timeout applies; the listener’s own idle-timeout setting doesn’t
reach the upgraded socket.
A client, from configuration
A client is configured only whenacp.client.transport.type, .stdio.command, .websocket.uri, or
.http.uri is set, giving three beans: the transport, an AcpAsyncClient, and an AcpSyncClient
facade over that same client (one session on one connection). Setting some other
acp.client.transport.* property alone, such as .websocket.connect-timeout or .stdio.args with no
.stdio.command, creates no client at all, rather than one that then fails for lacking a transport.
The client connects when you call initialize(), and closes gracefully, once, with the context.
Customize it before it’s built with AcpClientCustomizer beans, applied in @Order:
The handler pairing is enforced, not just advised: setting a capability (other than
boolean-config-options, which needs no handler) true without registering its handler fails bean
creation, naming the property to turn off or the handler to add, for example “fs.readTextFile needs
readTextFileHandler… The capabilities come from acp.client.capabilities.read-text-file=true”. A
customizer’s own sessionUpdateHandler replaces the bean’s DEBUG-logging one rather than running
alongside it.AcpClientCustomizer and AcpTransportType (used above by acp.client.transport.type) are not
Micronaut-specific types: both live in com.agentclientprotocol.sdk.integration, the
framework-neutral module Spring Boot and Quarkus 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.
Extension Beans
Beans of typeAcpInterceptor, ArgumentResolver, and ReturnValueHandler are added to the agent,
in bean order, the Micronaut-native way to extend the handler pipeline without subclassing anything.
Where handlers run
On JDK 21 and later, handler methods run on Micronaut’s virtual-thread executor (TaskExecutors.VIRTUAL), a bean this module looks up by name rather than something it builds itself;
on JDK 17 they run on the SDK’s own default pool (see
Concepts: Where handlers run) instead. This is
automatic, not a property: there’s no configuration to point handlers at a different executor, and in
particular not at Micronaut’s TaskExecutors.BLOCKING (the I/O pool), whose non-daemon threads would
keep a stdio application’s process alive past the point its context closes.
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; set that logger to WARN in your
logback.xml to silence it.
Dependencies
The Micronaut modules import the Micronaut 4.10 platform BOM; the SDK’s own Reactor, Jackson, Jetty, and JUnit versions take precedence over it. The wire format is the SDK’s own Jackson mapper (see JSON Mappers), not Micronaut Serialization.Related
Spring Boot Starter
The equivalent integration for Spring Boot applications
Clients and Agents in Java
The annotation model this module builds on
Transports
Where ACP listens in every deployment shape, Micronaut included