Go library for the Agent Client Protocol (ACP) - a standardized communication protocol between code editors and AI‑powered coding agents.
Learn more about the protocol itself at https://agentclientprotocol.com.
go get github.com/coder/acp-go-sdk@v0.13.5Start by reading the official ACP documentation to understand the core concepts and protocol specification.
The examples directory contains simple implementations of both Agents and Clients in Go. You can run them from your terminal or connect to external ACP agents.
go run ./example/agentstarts a minimal ACP agent over stdio.go run ./example/claude-codedemonstrates bridging to Claude Code.go run ./example/clientconnects to a running agent and streams a sample turn.go run ./example/geminibridges to the Gemini CLI in ACP mode (flags: -model, -sandbox, -yolo, -debug, -gemini /path/to/gemini).
You can watch the interaction by running go run ./example/client locally.
Browse the Go package docs on pkg.go.dev for detailed API documentation:
If you're building an Agent:
- Implement the
acp.Agentinterface (and optionallyacp.AgentLoaderforsession/load). - Create a connection with
acp.NewAgentSideConnection(agent, os.Stdout, os.Stdin). - Send updates and make client requests using the returned connection.
If you're building a Client:
- Implement the
acp.Clientinterface. It includes the terminal methods (CreateTerminal,TerminalOutput,ReleaseTerminal,WaitForTerminalExit,KillTerminal). Optionally implementacp.ClientExperimentalfor the unstable client methods. - Launch or connect to your Agent process (stdio), then create a connection with
acp.NewClientSideConnection(client, stdin, stdout). - Call
Initialize,NewSession, andPromptto run a turn and stream updates.
Helper constructors are provided to reduce boilerplate when working with union types:
- Content blocks:
acp.TextBlock,acp.ImageBlock,acp.AudioBlock,acp.ResourceLinkBlock,acp.ResourceBlock. - Tool content:
acp.ToolContent,acp.ToolDiffContent,acp.ToolTerminalRef. - Utility:
acp.Ptr[T]for pointer fields in request/update structs.
ACP supports extension methods for custom JSON-RPC methods whose names start with _.
Use them to add functionality without conflicting with future ACP versions.
Implement acp.ExtensionMethodHandler on your Agent or Client. Your handler will be
invoked for any incoming method starting with _.
// HandleExtensionMethod handles ACP extension methods (names starting with "_").
func (a MyAgent) HandleExtensionMethod(ctx context.Context, method string, params json.RawMessage) (any, error) {
switch method {
case "_example.com/hello":
var p struct {
Name string `json:"name"`
}
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return map[string]any{"greeting": "hello " + p.Name}, nil
default:
return nil, acp.NewMethodNotFound(method)
}
}Note: Per the ACP spec, unknown extension notifications should be ignored. This SDK suppresses noisy logs for unhandled extension notifications that return “Method not found”.
From either side, use CallExtension / NotifyExtension on the connection.
raw, err := conn.CallExtension(ctx, "_example.com/hello", map[string]any{"name": "world"})
if err != nil {
return err
}
var resp struct {
Greeting string `json:"greeting"`
}
if err := json.Unmarshal(raw, &resp); err != nil {
return err
}
if err := conn.NotifyExtension(ctx, "_example.com/progress", map[string]any{"pct": 50}); err != nil {
return err
}Note: A request returns after the handlers of all notifications that arrived before its response have returned. To make a request from a notification handler, such as
SessionUpdate, pass the handler'sctxor a ctx derived from it. With any other ctx, the request waits for the handler that made it, so it returns only when that ctx ends or the peer disconnects. A goroutine that the handler starts with the handler'sctxalso skips the wait for later notifications while the handler runs: give it a ctx that does not derive from the handler'sctxif it needs the full wait.
ACP uses the _meta field inside capability objects as the negotiation/advertising
surface for extensions.
- Client -> Agent:
InitializeRequest.ClientCapabilities.Meta - Agent -> Client:
InitializeResponse.AgentCapabilities.Meta
The ACP spec
reserves the root-level _meta keys traceparent, tracestate, and baggage for W3C
trace context/OpenTelemetry compatibility. This SDK does not read or set these keys.
For a complete, production‑ready integration, see the
Gemini CLI Agent which exposes an
ACP interface. The Go example client example/gemini demonstrates connecting
to it via stdio.
Package acp (module github.com/coder/acp-go-sdk, go 1.21, no require directives) implements both ends of ACP as JSON-RPC 2.0 messages, one JSON object per line.
Connection reads lines from the peer, routes requests, responses, and notifications, and serializes writes [connection.go:Connection].
AgentSideConnection and ClientSideConnection wrap a Connection and dispatch inbound methods to your Agent or Client implementation [agent.go:AgentSideConnection] [client.go:ClientSideConnection].
The wire types, method-name constants, inbound dispatch switches, outbound wrapper methods, and union constructors live in the *_gen.go files.
cmd/generate writes these files from the schemas in schema/, and schema/version pins the ACP release.
The hand-written files add errors, extension methods, and builder helpers.
- Connections:
NewAgentSideConnection(agent Agent, peerInput io.Writer, peerOutput io.Reader) *AgentSideConnectionandNewClientSideConnection(client Client, peerInput io.Writer, peerOutput io.Reader) *ClientSideConnection. Both types haveDone() <-chan struct{}andSetLogger(*slog.Logger)[agent.go:NewAgentSideConnection] [client.go:NewClientSideConnection] - Interfaces you implement:
Agent,AgentLoader(LoadSession),AgentExperimental(theUnstable*agent methods),Client, andClientExperimental(theUnstable*client methods) [types_gen.go:Agent, AgentLoader, AgentExperimental, Client, ClientExperimental]. OptionallyExtensionMethodHandlerfor_-prefixed methods [extensions.go:ExtensionMethodHandler] - Outbound calls:
ClientSideConnectionhas one method per agent method, for exampleInitialize,NewSession,Prompt, andCancel[client_gen.go:Initialize, NewSession, Prompt, Cancel].AgentSideConnectionhas one method per client method, for exampleSessionUpdate,RequestPermission,ReadTextFile, andCreateTerminal[agent_gen.go:SessionUpdate, RequestPermission, ReadTextFile, CreateTerminal]. Both types haveCallExtensionandNotifyExtension[extensions.go:CallExtension, NotifyExtension] - Exception:
mcp/messagehas the constantsAgentMethodMcpMessageandClientMethodMcpMessage, but no wrapper method and no dispatch case (#76). An inboundmcp/messagerequest gets a-32601reply from thedefaultcase. The notification form gets no reply, and the connection logs it at Error [constants_gen.go:AgentMethodMcpMessage, ClientMethodMcpMessage] [agent_gen.go:handle] [client_gen.go:handle] [connection.go:handleInbound] - Low-level transport:
Connection,NewConnection(handler MethodHandler, peerInput io.Writer, peerOutput io.Reader),MethodHandler,SendRequest[T](a generic function that takes the*Connection),(*Connection).SendRequestNoResult,(*Connection).SendNotification,(*Connection).SetLogger, and(*Connection).Done[connection.go:NewConnection, MethodHandler, SendRequest, SendRequestNoResult, SendNotification, SetLogger, Done] - Wire types: one struct per request, response, and notification, union wrappers with one pointer field per variant, and named
stringenums with constants such asStopReason,ToolKind,ToolCallStatus, andPermissionOptionKind[types_gen.go:StopReason, ToolKind, ToolCallStatus, PermissionOptionKind].ExtNotification,ExtRequest,ExtResponse, andUnstableMessageMcpResponseareany, so they have no generated fields and noValidate[types_gen.go:ExtNotification, ExtRequest, ExtResponse, UnstableMessageMcpResponse] - Unstable types: the types of unstable-only methods, and the definitions they reach that are new or changed, get the
Unstableprefix. Definitions that stable methods reach keep their schema names, even when they exist only inschema/schema.unstable.json, for exampleCostandAuthEnvVar[cmd/generate/internal/load/merge.go:buildUnstableDuplicateSet, promoteChangedSharedDefs, ensureRefsAvailable] - Constants:
ProtocolVersionNumber(1), plus 28AgentMethod*and 14ClientMethod*wire-name constants, for exampleAgentMethodSessionPrompt(session/prompt) [constants_gen.go:ProtocolVersionNumber, AgentMethodSessionPrompt] - Errors:
RequestError(Code,Message,Data) and its constructorsNewParseError(-32700),NewInvalidRequest(-32600),NewMethodNotFound(-32601),NewInvalidParams(-32602),NewInternalError(-32603),NewRequestCancelled(-32800), andNewAuthRequired(-32000) [errors.go:RequestError].NewMethodNotFoundsetsDatato{"method": method}[errors.go:NewMethodNotFound].(*RequestError).Errorreturns the JSON of code, message, and data,<nil>for a nil receiver, and the textcode %d: %s(with(data: %v)appended whenDatais set) when marshalling fails [errors.go:Error] - Builders: content blocks (
TextBlock,ImageBlock,AudioBlock,ResourceLinkBlock,ResourceBlock), tool content (ToolContent,ToolDiffContent,ToolTerminalRef), session updates (UpdateUserMessage,UpdateAgentMessage,UpdateAgentThought, their*Textforms, andUpdatePlan), tool calls (StartToolCall,StartReadToolCall, andStartEditToolCallwithWithStart*options,UpdateToolCallwithWithUpdate*options), andPtr[T][helpers.go]. GeneratedNew<Union><Variant>constructors, for exampleNewRequestPermissionOutcomeSelected[helpers_gen.go:NewRequestPermissionOutcomeSelected]
- The
*_gen.gofiles start withCode generated by acp-go-generator; DO NOT EDIT.Changecmd/generateinstead of editing them, then runcd cmd/generate && env -u GOPATH -u GOMODCACHE go run ..make versionruns the generator too, but only whenversionis older than a schema file or this README, so a generator change alone does not trigger it [constants_gen.go] [types_gen.go] [agent_gen.go] [client_gen.go] [helpers_gen.go] - Notifications from the peer run one at a time, in arrival order, on one goroutine [connection.go:processNotifications]
- A request from the peer starts its handler only after the handlers of all notifications that arrived before it have finished. There are two exceptions. First, it also starts while the running notification handler waits for a peer response, because the peer can need this answer first. Second, when the peer disconnects during the wait, the handler starts at once with a cancelled ctx, so it can clean up while earlier notification handlers still run [connection.go:receive, waitInboundBarrier, waitNotifications, startPeerWait]
- After a response arrives,
SendRequestandSendRequestNoResultreturn only after the handlers of all notifications that arrived before the response have finished, or when the ctx ends or the peer disconnects. Before a response, they return at once when the params do not marshal (-32602) or the write fails (-32603). In a notification handler, a request made with the handler's ctx (or a ctx derived from it) waits only for the earlier notifications [connection.go:SendRequest, SendRequestNoResult, prepareRequest] - The read loop handles
$/cancel_requestfrom the peer at once, outside notification order. It cancels the in-flight request with the same id and ignores an unknown id [connection.go:receive, handleCancelRequest] - When the ctx of an outbound request ends before the response, the connection sends
$/cancel_requestfor that id to the peer withcontext.Background(). At most 1024 cancels wait in the queue (maxPendingCancelRequests). Extra cancels are dropped with a Debug log [connection.go:waitForResponse, sendCancelRequest, sendCancelRequests] - JSON-RPC ids must be a string or a number. A
nullid unmarshals as no id: a request with"id": nullruns as a notification and gets no reply, and a response with"id": nullis logged and skipped. Numeric ids are canonicalized, so equal values match one pending request. A numeric id can have at most 4096 digits in canonical form and an exponent magnitude of at most 4096 (maxCanonicalJSONRPCIDKeyLen,maxCanonicalJSONRPCIDAbsExp10). When an inbound request id fails this check, the connection logs it and uses the raw id text [connection.go:canonicalJSONRPCIDKey, receive] - The connection closes when more than 1024 notifications are queued (
defaultMaxQueuedNotifications) or when an input line, without its newline, is 10 MiB (maxBufSize) or longer. Other bad input does not close it: the read loop skips blank lines, and it logs and skips a line that is not valid JSON or a message with neither id nor method [connection.go:receive] - On disconnect,
Done()closes at once. Waiting callers fail, and the contexts of inbound request handlers (derived from the connection context) are cancelled at the same time. Queued notifications then drain for up to 5 s, and only then are the contexts of notification handlers cancelled [connection.go:shutdownReceive, receive, processNotifications, notificationQueueDrainTimeout] - For the generated ACP methods, inbound params are unmarshalled into the generated type and checked with
Validate(). Extension methods (_-prefixed) reachExtensionMethodHandleras the rawjson.RawMessage, so the handler must decode and validate them itself [extensions.go:handleWithExtensions]. For a request, a failure returns-32602(NewInvalidParams), and an unknown method returns-32601(NewMethodNotFound), as does a method ofAgentLoader,AgentExperimental, orClientExperimentalthat your implementation does not have. A notification never gets a reply: the connection logs these errors at Error instead [agent_gen.go:handle] [client_gen.go:handle] [connection.go:handleInbound] - Handler errors pass through
toReqErr: a*RequestErroris sent unchanged,context.Canceledbecomes-32800, and any other error becomes-32603[errors.go:toReqErr] - Extension method names must start with
_.CallExtensionandNotifyExtensionreject other names before they send. Inbound_methods go toExtensionMethodHandler. When it is not implemented, a_request gets-32601, and a_notification is dropped with no reply and no log line. Other unhandled notifications are logged at Error [extensions.go:validateExtensionMethodName, handleWithExtensions] [connection.go:handleInbound] - Agent side: each inbound
session/promptruns with its own cancellable ctx, and an inboundsession/cancelcancels that ctx beforeAgent.Cancelruns. A prompt that arrived before a cancel but registers after it is cancelled at once. At most 1024 of these cancel markers are kept [agent_gen.go:handle] [agent.go:registerPrompt, cancelPrompt, maxCancelMarkers] - Client side: when
ClientSideConnection.Promptfails after its ctx ended, it sendssession/cancelwithcontext.Background()[client_gen.go:Prompt] - Union wrappers:
Validateon aoneOfunion requires exactly one variant to be set, andUnmarshalJSONreturnsno matching variant for unionwhen no variant fits [types_gen.go:ContentBlock.Validate, SessionUpdate.UnmarshalJSON] - Capability structs fill schema defaults on decode when a key is missing or
null. For example, a missingClientCapabilities.fsbecomes{"readTextFile":false,"writeTextFile":false}[types_gen.go:ClientCapabilities.UnmarshalJSON] StartReadToolCallapplies kindread, statuspending, one location, andrawInput {"path"}before the caller's options.StartEditToolCallapplies kindedit, statuspending, one location, andrawInput {"content", "path"}[helpers.go:StartReadToolCall, StartEditToolCall]
NewConnectionstarts 4 goroutines: a ctx-done broadcaster, the cancel sender, the read loop, and the notification processor. Each inbound request runs in its own goroutine [connection.go:NewConnection, receive]- The read loop reads the peer output with a
bufio.Scanner(1 MiB initial buffer, 10 MiB maximum line).sendMessagewrites each message plus\nto the peer input while it holdswriteMu[connection.go:receive, sendMessage] - Diagnostics go to the logger from
SetLogger, or toslog.Default()when none is set [connection.go:loggerOrDefault] - The connection panics when an internal sequence invariant breaks, for example a notification that would run out of order [connection.go:processNotifications, startPeerWait]
WithStartLocationswith exactly one location that has a non-emptyPathmirrors that path intoRawInput. It creates{"path": ...}whenRawInputis nil, adds apathkey to a caller-suppliedmap[string]anythat has none, and leaves any otherRawInputunchanged [helpers.go:WithStartLocations]
- cmd: ./cmd/README.md - Command-line programs.
cmd/generateis a separate Go module that readsschema/and writes the root*_gen.gofiles. - example: ./example/README.md - Four
package mainprograms on the publicacpAPI: a scripted agent, a demo client, and REPL clients for Claude Code and the Gemini CLI. - schema (no contract file): the ACP JSON schemas (
meta.json,schema.json,meta.unstable.json,schema.unstable.json) and theversionpin. Input ofcmd/generate. - testdata (no contract file): golden JSON payloads in
testdata/json_golden, used byjson_parity_test.go.
schema/versionpins the ACP release.make versionregenerates themdshblock in this README, downloads the schemas of that release, runscmd/generate, runsgofumpt -w ., and writes the rootversionstamp. Commit the schemas, the generated Go, andversiontogether.make releasefails whenschema/versionandversiondiffer [Makefile:version, release]- The generated
handleswitches use unexported names from the hand-written root files:registerPrompt,cancelPrompt,sessionCancels, andmu[agent.go], andtoReqErr[errors.go]. Rename them only together withcmd/generate/internal/emit[cmd/generate/internal/emit/dispatch.go] [cmd/generate/internal/emit/dispatch_helpers.go] cmd/generateemits no constructors forContentBlock,ToolCallContent, andSessionUpdate. Their builders are hand-written inhelpers.go[cmd/generate/internal/emit/helpers.go:WriteHelpersJen]- The root module uses only the standard library. Only
cmd/generatedepends ongithub.com/dave/jennifer[go.mod] [cmd/generate/go.mod] - The examples import only
github.com/coder/acp-go-sdkfrom this module, so they exercise the public API only [example/agent/main.go] [example/client/main.go] [example/claude-code/main.go] [example/gemini/main.go]
make versionis a file-stamp target: it runs only when a prerequisite is newer thanversion. It downloads a schema file fromhttps://github.com/agentclientprotocol/agent-client-protocol/releasesonly when that file is older thanschema/version. When the release has no unstable schema file (HTTP 404), it writes an empty placeholder ({"agentMethods":{},"clientMethods":{},"protocolMethods":{}}or{"$defs":{}}) and continues. Its first prerequisite rewrites this README throughmdshwhenschema/versionis newer than the README. The recipe then runscmd/generate, which overwrites the five*_gen.gofiles, reformats the Go files withgofumpt, and writesversion[Makefile:version, README.md]- The SDK speaks ACP only over the
io.Readerandio.Writeryou pass in. Its diagnostics (for example an Info line on every disconnect, and Error lines for malformed input) go toslog.Default(), which writes to stderr unless you change it, or to the logger set withSetLogger. The root package opens no file, network connection, or subprocess. The examples do: they start child processes (npx,gemini,go run) and read and write local files when the agent asks
- Run:
make test(runsgo test ./...,go build ./example/..., and theacpwiredriver tests) - Run:
cd cmd/generate && go test ./...(separate module, not covered bymake test) - Run:
make check(runstreefmt, thengit diff --exit-code. It needs the formatters frommise install)
Apache 2.0. See LICENSE.