ErrorCode definition. This
page is where this SDK writes them down.
The rule
A handler throwsAcpProtocolException (com.agentclientprotocol.sdk.error) to answer a request
with an error; the SDK converts it to a JSON-RPC error response before sending it. Everything else is
what a caller sees:
Some Javadoc on
AcpProtocolException still shows it being caught after a client call, from before
0.80.0, when AcpError was split out as its own type and the SDKâs own local rejections were unified
onto it too. A handler throwing AcpProtocolException is now the whole of that typeâs job: a caller
never catches it, only the AcpError the SDK builds from it on the wire.One base class for every one of these: AcpException
AcpError, AcpProtocolException, AcpCapabilityException, AcpConnectionException, and
AcpTimeoutException all extend com.agentclientprotocol.sdk.error.AcpException, so
catch (AcpException e) handles every failure of a call in one place, whichever of these it turns out
to be. AcpError extending AcpException is new: it used to extend RuntimeException directly, which
meant catch (AcpException e), the one place the SDK itself documented for handling a callâs failures,
silently missed every error the peer actually answered with.
CancellationException (java.util.concurrent) is the one type in the table above that is not an
AcpException: itâs a JDK type reused for interrupt propagation, not an SDK-specific failure.
What a handlerâs own exception becomes
Four different outcomes, depending on what the handler throws or lets escape:AcpProtocolException: sent as the handler intended, code, message and data unchanged. The callerâsAcpErrorcarries exactly what was thrown.- An
AcpErrorthe handler received from its own call to the peer, left to escape: passed on unchanged, not rewrapped. A handler that calls out, finds nothing of its own to add, and lets the failure propagate does not need to convert it: the caller sees the original code, message and data, not a flattened-32603. - A
CancellationException, an interrupt, or anAcpProtocolExceptionwith code-32800: all read as the handler cancelling its own work, ACP v1âs internal cancellation, and are answered-32800with the exceptionâs own message (or"Request cancelled"if it has none), not flattened to-32603the way an ordinary exception is. A handler that catches an interrupt from the SDK cancelling it and simply lets it propagate already gets the right answer without doing anything else. - Anything else (a bare
RuntimeException, a bug, anError): answered-32603with the generic message"Internal error"only. The exceptionâs own message is not sent to the peer (a security fix: a database error, a file path, or a URL with credentials could otherwise reach whatever sent the request). The real exception, with its stack trace, is logged atWARNon the side that handled the request, so itâs still visible to whoever runs that side.
AcpProtocolException with that message explicitly; nothing else but a cancellation carries a message
to the peer anymore.
Telling the peerâs error from the SDKâs own rejection
AcpError covers two different causes, and getData() is how to tell them apart. A peerâs actual
error response carries the peerâs own code, message and data, unchanged. A response the SDK rejected
locally always has code -32603 (JSON-RPC 2.0 defines no code for an invalid response) and a data
map with a "reason" and the requestâs "method":
{}
silently read as a PromptResponse with a null stopReason. A result is now checked the same way, on
both sides and down into nested records. AcpError.rejectedResponse(method, reason, message, detail)
builds the error for the three response-shape reasons above; itâs public because the SDKâs own two
sides both call it, not for application code to throw (a handler throwing an error uses
AcpProtocolException instead, which the SDK never rejects as malformed, since it reads from the
handlerâs own return value, not off the wire).
AcpError.getCode() returns the numeric code. getMessage() is the peerâs message text, plus any
detail from the errorâs data, with the code left out on purpose, so e.getCode() + " " + e.getMessage()
names an error exactly once; toString() (what a stack trace prints) includes the code.
The v1 error codes
Every code in 0.80.0 matches the ACP v1 schema exactly; nothing is this SDKâs own invention.
All seven live on
AcpErrorCodes as int constants; throw AcpProtocolException with one of them
from a handler, and compare AcpError.getCode() against them as a caller.
Converting between the two
JSONRPCError.from(...) over hand-building a wire error; itâs what the SDKâs own dispatch code
uses to turn a thrown AcpProtocolException into the response it sends. toException() is a public
utility with no internal caller of its own, useful if youâre building something (a proxy, a bridge)
that needs to re-throw a peerâs error using this SDKâs exception type rather than AcpError.
Two more exceptions, specific to the sync API
A blocking call (AcpSyncClient, AcpSyncAgent, SyncPromptContext) can also fail with one of two
exceptions that have nothing to do with a JSON-RPC error at all, peer-sent or locally detected:
com.agentclientprotocol.sdk.error.AcpTimeoutException (an AcpException) replaces what used to
escape as Reactorâs internal Exceptions$ReactiveException. java.util.concurrent.CancellationException
is thrown, with the threadâs interrupt flag left set, when a blocked callâs thread is interrupted, the
same mechanism the SDK itself uses to cancel a sync handler. The async API doesnât throw either of
these: its Mono fails with the plain TimeoutException directly.
Timeouts and interrupts keep their own types rather than folding into
AcpError: neither is a
JSON-RPC error at all, peer-sent or locally detected, so thereâs nothing for AcpErrorâs code and
data to describe.A lost connection: AcpConnectionException
A request can fail for a reason that has nothing to do with a JSON-RPC answer, peer-sent or locally
detected, and nothing to do with a timeout either: the connection itself is gone, on either side.
com.agentclientprotocol.sdk.error.AcpConnectionException covers every case where a message canât be
carried because the connection is gone, on the client and the agent side alike: a request still
waiting for its answer when the transport ends ("ACP session with agent terminated"), and a request
or notification sent after that ("ACP client transport is not connected: ...") or after close()
("The transport is closed"). Before 0.80.0 these were a plain RuntimeException and an
IllegalStateException respectively, so catch (AcpConnectionException e) missed the commonest
connection failure, a peer that simply went away. Its cause, when there is one, is why the connection
ended, such as the transportâs own failure, or a stdio agentâs exit code
("ACP agent process exited with code 137 (signal 9)" from awaitTermination()). A closed transport
does not reopen: connect a new transport and client or agent to carry on.
Building a client or agent on a transport that refuses to connect or start at once (one already in
use) still throws
IllegalStateException, not AcpConnectionException: thatâs a misuse of the
transport, not a connection that was once alive and is now lost.Two exceptions that mean the request was never sent
AcpCapabilityException and IllegalStateException both happen before a request leaves: capability
negotiation, or connection lifecycle, ruled the call out, so there was nothing to send and nothing to
time out.
initialize fails locally with AcpCapabilityException, naming the
capability, when the agentâs initialize answer didnât advertise it: loadSession, listSessions,
closeSession, deleteSession, resumeSession, forkSession, logout, and the providers/* calls.
It also covers a session/new, session/load, session/resume, or session/fork that names
non-empty additionalDirectories when the agent doesnât advertise
sessionCapabilities.additionalDirectories: check getAgentCapabilities().supportsAdditionalDirectories()
before naming any. An agent call works the same way in the other direction for a client capability the
client didnât advertise: readTextFile, writeTextFile, any of the five terminal methods (create,
output, waitForExit, kill, and release, all checked as of 0.80.0, not only create), or
createElicitation for a mode the client didnât announce, covered on
Agent-to-Client Calls and
Elicitation.
AcpCapabilityException.toProtocolException() is removed: it answered -32600, a code the SDK made
up for the case, while the one refusal ACP actually specifies (a client asked for an elicitation mode
it never declared) answers -32602 directly, which the SDKâs client already does without this method.
A handler that needs to answer its own capability check as an error throws AcpProtocolException
itself: new AcpProtocolException(AcpErrorCodes.INVALID_PARAMS, e.getMessage(), e.getCapability()).AcpAsyncClient/AcpSyncClient call but initialize itself fails with IllegalStateException
(âCall initialize() firstâ) until initialize has answered; before 0.80.0 it was sent anyway, relying
on the agent to reject it. The check runs when the call is subscribed, not when itâs
built, so client.initialize().then(client.newSession(request)) is fine without waiting for the
first call to complete first. Extension calls (sendExtRequest, sendExtNotification) are outside
ACPâs lifecycle and skip both checks.
Related
Forward Compatibility
Open value types and Unknown* variants: the other half of staying safe against a newer peer
Migration Guide
Every error-code renumbering from 0.18.0, in one table