Skip to content

Add Go SDK and generator#56

Closed
ThomasK33 wants to merge 31 commits intoagentclientprotocol:mainfrom
coder:main
Closed

Add Go SDK and generator#56
ThomasK33 wants to merge 31 commits intoagentclientprotocol:mainfrom
coder:main

Conversation

@ThomasK33
Copy link
Copy Markdown

Overview

This PR introduces a Go SDK for ACP, including terminal support, context-aware APIs with cancellation, a modular code generator, structured error handling, JSON parity tests with golden files, and updated docs/examples.

What’s Supported

  • Go SDK: Agent/Client interfaces, connection management, request/response dispatch, generated types/constants.
  • Terminal: Dedicated ClientTerminal interface and terminal content handling.
  • Context & Cancellation: All interface methods accept context.Context with cancellation propagation.
  • Content Types: Text, audio, image, diffs (with/without old), resource blobs/links/text, terminal content.
  • Session Flow: Initialize, new session, updates (agent/user message chunks, plan, tool calls: read/edit/locations/rawinput/update).
  • Filesystem: Read/write text file request/response types.
  • Permissions: Request permission flow and outcomes (selected/cancelled).
  • Errors: JSON-formatted RequestError for structured client consumption.
  • Codegen: Internal IR + emit phases generating *_gen.go for types, constants, helpers, and interfaces.

Implementation Notes

  • Interface Structure: Methods grouped into stable, experimental, and optional for cleaner API surfaces.
  • Terminal Split: Terminal-related methods factored into a standalone ClientTerminal interface.
  • Codegen Architecture: Modular generator at go/cmd/generate/internal/{ir,emit,load,util} with simplified dispatch helpers and signatures.
  • Types & Unions: Optional fields are modeled with pointers; unions are generated to encode optionality accurately.
  • Helpers Extraction: Shared helper logic moved to static go/helpers.go (reduces generated code size).
  • Examples: Agent, client, Gemini, and Claude Code examples demonstrate typical usage.

Testing

  • Parity Tests: Golden-file JSON parity tests in go/testdata/json_golden cover content variants, FS ops, session updates, permissions, tool‑call forms, and terminal content.
  • Constructors Validation: Tests verify that constructor helpers and generated constructors match the golden JSON.
  • Example Tests: Example-based tests for agent/client/gemini flows validate end-to-end behavior.

Docs & Examples

  • Go README: The installation and usage guide is under go/README.md.
  • Docs Page: Go library included in docs/libraries/go.mdx with package info and examples.
  • Samples: Updated and expanded examples (agent, client, gemini, Claude Code) using context-aware APIs.

Change-Id: Ic3a8ca18551872870ebc922dcdce7ec1669c9fd6
Signed-off-by: Thomas Kosiewski tk@coder.com

Change-Id: Ic3a8ca18551872870ebc922dcdce7ec1669c9fd6
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I17041dcb362750bad7e95ab5c84ab707dfaec85f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…nal methods

Change-Id: I4d1ebe3b6f5b53a52e1ec9c13e4028b4d05a6b5e
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I6f5b08f6e93cd8fc5b904ab91014c206647a4aca
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…and add Claude Code example

Change-Id: I7ee9a6217c3c33ef27e5143acf8e2f9e17ded3dc
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…h handlers

Change-Id: Ie1670abd513fab42dedaa2c9e55362840f2f9985
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ntation

Change-Id: I337b07eea029a16481cf41869f9a327f9184c5fa
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Idbc02da87f6925b2fd847ffe1a04fcdda33b2162
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: If82530aca4ae621a6d8200cebbdd50e144f57f24
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ancellation

Change-Id: Ic242f8ab12e3760e7ef67b70385f8db1cf5a0262
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I2bdd3b643953b5ef1856e73285087d7976a130ec
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Iaf75a10da7807d196dde16ec744c72182d767dd2
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Iad1f284ea06288dd72e8eab3b6c429ea88113fee
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Idf60f2e210f012cb035aa4de5680a1fbcf84d4c5
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…alidation

Change-Id: I9363d69a0842e656ecbfeeba19f379206407d924
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…d JSON marshaling

Change-Id: Ia6003b4adb006cce9db9e6ab9b0a17294bfcfa44
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I410e8a8452949641864ca4e2e876ff2df6d71eee
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented Sep 2, 2025

We require contributors to sign our Contributor License Agreement, and we don't have @ThomasK33 on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@ThomasK33
Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Sep 2, 2025
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented Sep 2, 2025

The cla-bot has been summoned, and re-checked this pull request!

Change-Id: I07e46aa00b16f50b44e8e56b614c125cbf8bcb47
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ConradIrwin
Copy link
Copy Markdown
Contributor

ConradIrwin commented Sep 5, 2025

Thanks for this, it looks great!

One thing I noticed is that if you run the rust example client with the go example agent, it crashes because it returns "authMethods":null vs "authMethods":[] or just omitting the field. Not sure how easy that is to fix, but would be nice to have that work out of the box.

@deepakdinesh1123
Copy link
Copy Markdown

Thank you very much for this

I tried to use this to integrate crush in Zed editor (ref: https://github.com/deepakdinesh1123/crush/blob/feat/acp/internal/acpagent/acp.go), as soon as I launch a new thread with crush agent in Zed I get the error failed to deserialize response before a session is even created, might be related to the issue @ConradIrwin is mentioning if not please let me know if I am using the SDK incorrectly.

@ConradIrwin
Copy link
Copy Markdown
Contributor

@deepakdinesh1123 Does it work if you set AuthMethods to the empty array?

@deepakdinesh1123
Copy link
Copy Markdown

@ConradIrwin the typ acp.AuthenticateResponse is as follows

type AuthenticateResponse struct{}

and the method authenticate method signature is

func (a *ACPAgent) Authenticate(ctx context.Context, _ acp.AuthenticateRequest) error { return nil }

so I was not able to return any response other than the error as nil

@ConradIrwin
Copy link
Copy Markdown
Contributor

I think the problem is the InitializeResponse:

type InitializeResponse struct {
	AgentCapabilities AgentCapabilities `json:"agentCapabilities,omitempty"`
	AuthMethods []AuthMethod `json:"authMethods"` <--- THIS ONE
	ProtocolVersion ProtocolVersion `json:"protocolVersion"`
}

@ThomasK33
Copy link
Copy Markdown
Author

Hey @ConradIrwin, you're right.

Let me look into extending the codegen to either serialize to the default values specified in the schema.json file if it's the Go default value, or find another smart solution.

Updating the go/example/agent/main.go to include

func (a *exampleAgent) Initialize(ctx context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
	return acp.InitializeResponse{
		ProtocolVersion: acp.ProtocolVersionNumber,
		AgentCapabilities: acp.AgentCapabilities{
			LoadSession: false,
		},
		AuthMethods: []acp.AuthMethod{},
	}, nil
}

seems to fix the issue as you suggested.

@ConradIrwin
Copy link
Copy Markdown
Contributor

👍 Also happy to make changes to how the schema is generated if there's a way we can express "undefined is fine, but null causes rust's serde to throw a fit"

…o module restructure

Change-Id: Ic03480847e6d562067e0b170eb72fe6ea39efc20
Signed-off-by: Thomas Kosiewski <tk@coder.com>
… and reduced code duplication

Change-Id: I808b16216e09a94eca58cd8e95c99b3bb927596f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: If54609a9f26d3faef80d821b6d80fcd9696a4f62
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I5db6760df4ae4f89ebacc3f1cc83f15d95afd0b9
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
Copy link
Copy Markdown
Author

👍 Also happy to make changes to how the schema is generated if there's a way we can express "undefined is fine, but null causes rust's serde to throw a fit"

Since most of this is generated code, it's fine for now. The generator can handle serialization and deserialization of default values, working around Go's default value serialization issues.

Also, I added a Nix flake to set up a dev shell with all the necessary packages to run the npm scripts. Let me know if you’d prefer to remove it—I'm OK with that.

@ConradIrwin
Copy link
Copy Markdown
Contributor

Having a nix flake seems fine (assuming I don't have to use it :D).

Do the latest set of changes fix the serialization thing? Is there anything else you wanted to tidy up before merging this?

@deepakdinesh1123
Copy link
Copy Markdown

@ThomasK33

I am getting this error when I try to build a version of crush that is using this module

invalid receiver type SetSessionModeResponse  (pointer or interface type) (compiler InvalidRecv)

due to this in types_gen.go

type SetSessionModeResponse any

func (v *SetSessionModeResponse) Validate() error {
	return nil
}

Change-Id: I8b0d3562a5abd7f61416eb4efd8f6b52c0c3f3c8
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I6c29fa534f61d9571b61c95f6fc13a791700f9f1
Signed-off-by: Thomas Kosiewski <tk@coder.com>
… KillTerminalCommand

Change-Id: Ie34241477e4f04a55a57dd04639c68f9b2b6fcc3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@morgankrey
Copy link
Copy Markdown
Contributor

@ThomasK33 let us know if we can help get this over the line!

Change-Id: I190a4c47db0efdc54acae7e0ceceb711c85ade02
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I702ad8086d4da8253e8caca8d11ec8346e690bff
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ifb2d5b0a40cd17c0b014431d15852606700ea001
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I76bfcaaf9522c224c2d846535a11a2d99323d05c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I2505cb5e1175520c06d933455f995aa93bd11f58
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I1b5054a0dcfbde8002ba67d42f1f19a786d48a6f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
Copy link
Copy Markdown
Author

ThomasK33 commented Sep 22, 2025

@ThomasK33 let us know if we can help get this over the line!

Hey @morgankrey, sorry, I somehow did not see the ping.
I rebased and regenerated the Go parts; they should be good to merge.


@deepakdinesh1123 I also tested it against your crush fork with the acp subcommand.
I can use the following patch on your feat/acp branch to work in Zed and CLIs.

Inline Patch on `feat/acp`
diff --git a/internal/acpagent/acp.go b/internal/acpagent/acp.go
index e20a351d..c4c63481 100644
--- a/internal/acpagent/acp.go
+++ b/internal/acpagent/acp.go
@@ -12,6 +12,8 @@ import (
 	acp "github.com/zed-industries/agent-client-protocol/go"
 )
 
+var _ acp.Agent = (*ACPAgent)(nil)
+
 type ACPAgent struct {
 	app  *app.App
 	conn *acp.AgentSideConnection
@@ -63,9 +65,10 @@ func (a *ACPAgent) Cancel(ctx context.Context, params acp.CancelNotification) er
 }
 
 func (a *ACPAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
-	_, err := a.app.Sessions.Get(ctx, string(params.SessionId))
+	sessionID := string(params.SessionId)
+	_, err := a.app.Sessions.Get(ctx, sessionID)
 	if err != nil {
-		return acp.PromptResponse{}, fmt.Errorf("session %s not found", string(params.SessionId))
+		return acp.PromptResponse{}, fmt.Errorf("session %s not found", sessionID)
 	}
 
 	content := ""
@@ -75,7 +78,7 @@ func (a *ACPAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Pr
 		}
 	}
 
-	done, err := a.app.CoderAgent.Run(context.Background(), string(params.SessionId), content)
+	done, err := a.app.CoderAgent.Run(ctx, sessionID, content)
 	if err != nil {
 		return acp.PromptResponse{}, err
 	}
@@ -134,6 +137,7 @@ func (a *ACPAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Pr
 					case message.BinaryContent:
 					case message.ImageURLContent:
 					case message.Finish:
+						return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
 					case message.TextContent:
 						// Only send the delta (new text content)
 						if len(part.Text) > len(lastTextSent) {
@@ -169,7 +173,13 @@ func (a *ACPAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Pr
 			}
 		}
 	}
+}
 
+func (a *ACPAgent) Authenticate(ctx context.Context, _ acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
+	return acp.AuthenticateResponse{}, nil
 }
 
-func (a *ACPAgent) Authenticate(ctx context.Context, _ acp.AuthenticateRequest) error { return nil }
+// SetSessionMode implements acp.Agent.
+func (a *ACPAgent) SetSessionMode(ctx context.Context, params acp.SetSessionModeRequest) (acp.SetSessionModeResponse, error) {
+	return acp.SetSessionModeResponse{}, nil
+}

@morgankrey
Copy link
Copy Markdown
Contributor

Awesome - @benbrandt @rtfeldman ready for review!

@benbrandt
Copy link
Copy Markdown
Member

Closing in favor of: https://github.com/coder/acp-go-sdk

@benbrandt benbrandt closed this Oct 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants