diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3be62f58..3796ba3b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,6 +31,14 @@ jobs:
node-version: latest
cache: "npm"
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+ cache: true
+ cache-dependency-path: |
+ go/cmd/generate/go.sum
+
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48
with:
diff --git a/.gitignore b/.gitignore
index 78fefd3d..806bc05e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,12 @@ typescript/*.js.map
# TypeDoc generated documentation
typescript/docs/
+
+# Go files
+.gocache
+.gopath
+
+.envrc
+.direnv
+.rustup/
+.cargo/
diff --git a/README.md b/README.md
index 3229a82e..c5e00142 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Learn more at [agentclientprotocol.com](https://agentclientprotocol.com/).
- **Rust**: [`agent-client-protocol`](https://crates.io/crates/agent-client-protocol) - See [examples/agent.rs](./rust/examples/agent.rs) and [examples/client.rs](./rust/examples/client.rs)
- **TypeScript**: [`@zed-industries/agent-client-protocol`](https://www.npmjs.com/package/@zed-industries/agent-client-protocol) - See [examples/](./typescript/examples/)
+- **Go**: [`github.com/zed-industries/agent-client-protocol/go`](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go) - See [example/](./go/example/) and the [Go README](./go/README.md)
- **JSON Schema**: [schema.json](./schema/schema.json)
- [**use-acp**](https://github.com/marimo-team/use-acp): React hooks for connecting to Agent Client Protocol (ACP) servers.
diff --git a/docs/docs.json b/docs/docs.json
index a2013f6a..d5f38d95 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -69,6 +69,7 @@
"pages": [
"libraries/typescript",
"libraries/rust",
+ "libraries/go",
"libraries/community"
]
},
diff --git a/docs/libraries/go.mdx b/docs/libraries/go.mdx
new file mode 100644
index 00000000..27e8bf89
--- /dev/null
+++ b/docs/libraries/go.mdx
@@ -0,0 +1,33 @@
+---
+title: "Go"
+description: "Go library for the Agent Client Protocol"
+---
+
+The [`github.com/zed-industries/agent-client-protocol/go`](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go)
+package provides implementations of both sides of the Agent Client Protocol that
+you can use to build your own agent server or client.
+
+To get started, add the module to your project:
+
+```bash
+go get github.com/zed-industries/agent-client-protocol/go@latest
+```
+
+Depending on what kind of tool you're building, you'll create either the
+[AgentSideConnection](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go#NewAgentSideConnection)
+or the
+[ClientSideConnection](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go#NewClientSideConnection)
+and implement the corresponding interfaces (`Agent`, `Client`).
+
+You can find example implementations of both sides in the
+[main repository](https://github.com/zed-industries/agent-client-protocol/tree/main/go/example).
+These can be run from your terminal or connected to external ACP agents, making
+them great starting points for your own integration!
+
+Browse the Go package docs on
+[pkg.go.dev](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go)
+for detailed API documentation.
+
+For a complete, production-ready implementation of an ACP agent, see the
+[Gemini CLI](https://github.com/google-gemini/gemini-cli) which exposes an ACP
+interface. The Go example client demonstrates connecting to it via stdio.
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..5fd14c90
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,96 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1757244434,
+ "narHash": "sha256-AeqTqY0Y95K1Fgs6wuT1LafBNcmKxcOkWnm4alD9pqM=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "092c565d333be1e17b4779ac22104338941d913f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1744536153,
+ "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "rust-overlay": "rust-overlay"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": "nixpkgs_2"
+ },
+ "locked": {
+ "lastModified": 1757298987,
+ "narHash": "sha256-yuFSw6fpfjPtVMmym51ozHYpJQ7SzVOTkk7tUv2JA0U=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "cfd63776bde44438ff2936f0c9194c79dd407a5f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..2585f915
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,78 @@
+{
+ description = "Devshell for ACP";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
+ flake-utils.url = "github:numtide/flake-utils";
+ rust-overlay.url = "github:oxalica/rust-overlay";
+ };
+
+ outputs =
+ {
+ self,
+ nixpkgs,
+ flake-utils,
+ rust-overlay,
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ rust-overlay.overlays.default ];
+ };
+
+ formatter = pkgs.nixfmt-rfc-style;
+
+ # Use rustup to manage toolchains so `cargo +nightly` works in dev shell
+ rustup = pkgs.rustup;
+ in
+ {
+ inherit formatter;
+
+ devShells.default = pkgs.mkShell {
+ packages = with pkgs; [
+ # Rustup manages stable/nightly toolchains according to rust-toolchain.toml
+ rustup
+ pkg-config
+ openssl
+
+ # Node.js toolchain
+ nodejs_24
+
+ # Go toolchain
+ go_1_24
+
+ # Nix formatter
+ formatter
+ ];
+
+ RUST_BACKTRACE = "1";
+
+ # Ensure rustup shims are used and install required toolchains on first entry
+ shellHook = ''
+ export RUSTUP_HOME="$PWD/.rustup"
+ export CARGO_HOME="$PWD/.cargo"
+ export PATH="$CARGO_HOME/bin:$PATH"
+
+ if ! command -v rustup >/dev/null 2>&1; then
+ echo "rustup not found in PATH" 1>&2
+ else
+ # Install toolchains if missing; respect pinned channel from rust-toolchain.toml
+ if ! rustup toolchain list | grep -q nightly; then
+ rustup toolchain install nightly --profile minimal >/dev/null 2>&1 || true
+ fi
+ # Ensure stable toolchain from rust-toolchain.toml exists (rustup will auto-select it)
+ # Attempt to install channel specified in rust-toolchain.toml (fallback to stable)
+ TOOLCHAIN_CHANNEL=$(sed -n 's/^channel\s*=\s*"\(.*\)"/\1/p' rust-toolchain.toml || true)
+ if [ -n "$TOOLCHAIN_CHANNEL" ]; then
+ if ! rustup toolchain list | grep -q "$TOOLCHAIN_CHANNEL"; then
+ rustup toolchain install "$TOOLCHAIN_CHANNEL" --profile minimal --component rustfmt clippy >/dev/null 2>&1 || true
+ fi
+ fi
+ fi
+ '';
+ };
+ }
+ );
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 00000000..f23c3f44
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/zed-industries/agent-client-protocol
+
+go 1.21
diff --git a/go/README.md b/go/README.md
new file mode 100644
index 00000000..c8e2d7e4
--- /dev/null
+++ b/go/README.md
@@ -0,0 +1,74 @@
+
+
+
+
+# ACP Go Library
+
+The official Go implementation of the Agent Client Protocol (ACP) — a standardized communication protocol between code editors and AI‑powered coding agents.
+
+Learn more at
+
+## Installation
+
+```bash
+go get github.com/zed-industries/agent-client-protocol/go@latest
+```
+
+## Get Started
+
+### Understand the Protocol
+
+Start by reading the [official ACP documentation](https://agentclientprotocol.com) to understand the core concepts and protocol specification.
+
+### Try the Examples
+
+The [examples directory](https://github.com/zed-industries/agent-client-protocol/tree/main/go/example) contains simple implementations of both Agents and Clients in Go. You can run them from your terminal or connect to external ACP agents.
+
+- Run the example Agent:
+ - `cd go && go run ./example/agent`
+- Run the example Client (connects to the example Agent):
+ - `cd go && go run ./example/client`
+- Connect to the Gemini CLI (ACP mode):
+ - `cd go && go run ./example/gemini -yolo`
+ - Optional flags: `-model`, `-sandbox`, `-debug`, `-gemini /path/to/gemini`
+- Connect to Claude Code (via npx):
+ - `cd go && go run ./example/claude-code -yolo`
+
+### Explore the API
+
+Browse the Go package docs on pkg.go.dev for detailed API documentation:
+
+-
+
+If you're building an [Agent](https://agentclientprotocol.com/protocol/overview#agent):
+
+- Implement the `acp.Agent` interface (and optionally `acp.AgentLoader` for `session/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](https://agentclientprotocol.com/protocol/overview#client):
+
+- Implement the `acp.Client` interface (and optionally `acp.ClientTerminal` for terminal features).
+- Launch or connect to your Agent process (stdio), then create a connection with `acp.NewClientSideConnection(client, stdin, stdout)`.
+- Call `Initialize`, `NewSession`, and `Prompt` to 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.
+
+### Study a Production Implementation
+
+For a complete, production‑ready integration, see the [Gemini CLI Agent](https://github.com/google-gemini/gemini-cli) which exposes an ACP interface. The Go example client `go/example/gemini` demonstrates connecting to it via stdio.
+
+## Resources
+
+- [Go package docs](https://pkg.go.dev/github.com/zed-industries/agent-client-protocol/go)
+- [Examples (Go)](https://github.com/zed-industries/agent-client-protocol/tree/main/go/example)
+- [Protocol Documentation](https://agentclientprotocol.com)
+- [GitHub Repository](https://github.com/zed-industries/agent-client-protocol)
+
+## Contributing
+
+See the main [repository](https://github.com/zed-industries/agent-client-protocol) for contribution guidelines.
diff --git a/go/acp_test.go b/go/acp_test.go
new file mode 100644
index 00000000..b7bf2a0e
--- /dev/null
+++ b/go/acp_test.go
@@ -0,0 +1,635 @@
+package acp
+
+import (
+ "context"
+ "io"
+ "slices"
+ "sync"
+ "testing"
+ "time"
+)
+
+type clientFuncs struct {
+ WriteTextFileFunc func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error)
+ ReadTextFileFunc func(context.Context, ReadTextFileRequest) (ReadTextFileResponse, error)
+ RequestPermissionFunc func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error)
+ SessionUpdateFunc func(context.Context, SessionNotification) error
+ // Terminal-related handlers
+ CreateTerminalFunc func(context.Context, CreateTerminalRequest) (CreateTerminalResponse, error)
+ KillTerminalCommandFunc func(context.Context, KillTerminalCommandRequest) (KillTerminalCommandResponse, error)
+ ReleaseTerminalFunc func(context.Context, ReleaseTerminalRequest) (ReleaseTerminalResponse, error)
+ TerminalOutputFunc func(context.Context, TerminalOutputRequest) (TerminalOutputResponse, error)
+ WaitForTerminalExitFunc func(context.Context, WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error)
+}
+
+var _ Client = (*clientFuncs)(nil)
+
+func (c clientFuncs) WriteTextFile(ctx context.Context, p WriteTextFileRequest) (WriteTextFileResponse, error) {
+ if c.WriteTextFileFunc != nil {
+ return c.WriteTextFileFunc(ctx, p)
+ }
+ return WriteTextFileResponse{}, nil
+}
+
+func (c clientFuncs) ReadTextFile(ctx context.Context, p ReadTextFileRequest) (ReadTextFileResponse, error) {
+ if c.ReadTextFileFunc != nil {
+ return c.ReadTextFileFunc(ctx, p)
+ }
+ return ReadTextFileResponse{}, nil
+}
+
+func (c clientFuncs) RequestPermission(ctx context.Context, p RequestPermissionRequest) (RequestPermissionResponse, error) {
+ if c.RequestPermissionFunc != nil {
+ return c.RequestPermissionFunc(ctx, p)
+ }
+ return RequestPermissionResponse{}, nil
+}
+
+func (c clientFuncs) SessionUpdate(ctx context.Context, n SessionNotification) error {
+ if c.SessionUpdateFunc != nil {
+ return c.SessionUpdateFunc(ctx, n)
+ }
+ return nil
+}
+
+// CreateTerminal implements Client.
+func (c *clientFuncs) CreateTerminal(ctx context.Context, params CreateTerminalRequest) (CreateTerminalResponse, error) {
+ if c.CreateTerminalFunc != nil {
+ return c.CreateTerminalFunc(ctx, params)
+ }
+ return CreateTerminalResponse{TerminalId: "test-terminal"}, nil
+}
+
+// KillTerminalCommand implements Client.
+func (c clientFuncs) KillTerminalCommand(ctx context.Context, params KillTerminalCommandRequest) (KillTerminalCommandResponse, error) {
+ if c.KillTerminalCommandFunc != nil {
+ return c.KillTerminalCommandFunc(ctx, params)
+ }
+ return KillTerminalCommandResponse{}, nil
+}
+
+// ReleaseTerminal implements Client.
+func (c clientFuncs) ReleaseTerminal(ctx context.Context, params ReleaseTerminalRequest) (ReleaseTerminalResponse, error) {
+ if c.ReleaseTerminalFunc != nil {
+ return c.ReleaseTerminalFunc(ctx, params)
+ }
+ return ReleaseTerminalResponse{}, nil
+}
+
+// TerminalOutput implements Client.
+func (c *clientFuncs) TerminalOutput(ctx context.Context, params TerminalOutputRequest) (TerminalOutputResponse, error) {
+ if c.TerminalOutputFunc != nil {
+ return c.TerminalOutputFunc(ctx, params)
+ }
+ return TerminalOutputResponse{Output: "ok", Truncated: false}, nil
+}
+
+// WaitForTerminalExit implements Client.
+func (c *clientFuncs) WaitForTerminalExit(ctx context.Context, params WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error) {
+ if c.WaitForTerminalExitFunc != nil {
+ return c.WaitForTerminalExitFunc(ctx, params)
+ }
+ return WaitForTerminalExitResponse{}, nil
+}
+
+type agentFuncs struct {
+ InitializeFunc func(context.Context, InitializeRequest) (InitializeResponse, error)
+ NewSessionFunc func(context.Context, NewSessionRequest) (NewSessionResponse, error)
+ LoadSessionFunc func(context.Context, LoadSessionRequest) (LoadSessionResponse, error)
+ AuthenticateFunc func(context.Context, AuthenticateRequest) (AuthenticateResponse, error)
+ PromptFunc func(context.Context, PromptRequest) (PromptResponse, error)
+ CancelFunc func(context.Context, CancelNotification) error
+ SetSessionModeFunc func(ctx context.Context, params SetSessionModeRequest) (SetSessionModeResponse, error)
+ SetSessionModelFunc func(ctx context.Context, params SetSessionModelRequest) (SetSessionModelResponse, error)
+}
+
+var (
+ _ Agent = (*agentFuncs)(nil)
+ _ AgentLoader = (*agentFuncs)(nil)
+ _ AgentExperimental = (*agentFuncs)(nil)
+)
+
+func (a agentFuncs) Initialize(ctx context.Context, p InitializeRequest) (InitializeResponse, error) {
+ if a.InitializeFunc != nil {
+ return a.InitializeFunc(ctx, p)
+ }
+ return InitializeResponse{}, nil
+}
+
+func (a agentFuncs) NewSession(ctx context.Context, p NewSessionRequest) (NewSessionResponse, error) {
+ if a.NewSessionFunc != nil {
+ return a.NewSessionFunc(ctx, p)
+ }
+ return NewSessionResponse{}, nil
+}
+
+func (a agentFuncs) LoadSession(ctx context.Context, p LoadSessionRequest) (LoadSessionResponse, error) {
+ if a.LoadSessionFunc != nil {
+ return a.LoadSessionFunc(ctx, p)
+ }
+ return LoadSessionResponse{}, nil
+}
+
+func (a agentFuncs) Authenticate(ctx context.Context, p AuthenticateRequest) (AuthenticateResponse, error) {
+ if a.AuthenticateFunc != nil {
+ return a.AuthenticateFunc(ctx, p)
+ }
+ return AuthenticateResponse{}, nil
+}
+
+func (a agentFuncs) Prompt(ctx context.Context, p PromptRequest) (PromptResponse, error) {
+ if a.PromptFunc != nil {
+ return a.PromptFunc(ctx, p)
+ }
+ return PromptResponse{}, nil
+}
+
+func (a agentFuncs) Cancel(ctx context.Context, n CancelNotification) error {
+ if a.CancelFunc != nil {
+ return a.CancelFunc(ctx, n)
+ }
+ return nil
+}
+
+// SetSessionMode implements Agent.
+func (a agentFuncs) SetSessionMode(ctx context.Context, params SetSessionModeRequest) (SetSessionModeResponse, error) {
+ if a.SetSessionModeFunc != nil {
+ return a.SetSessionModeFunc(ctx, params)
+ }
+ return SetSessionModeResponse{}, nil
+}
+
+// SetSessionModel implements AgentExperimental.
+func (a agentFuncs) SetSessionModel(ctx context.Context, params SetSessionModelRequest) (SetSessionModelResponse, error) {
+ if a.SetSessionModelFunc != nil {
+ return a.SetSessionModelFunc(ctx, params)
+ }
+ return SetSessionModelResponse{}, nil
+}
+
+// Test bidirectional error handling similar to typescript/acp.test.ts
+func TestConnectionHandlesErrorsBidirectional(t *testing.T) {
+ ctx := context.Background()
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ c := NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error) {
+ return WriteTextFileResponse{}, &RequestError{Code: -32603, Message: "Write failed"}
+ },
+ ReadTextFileFunc: func(context.Context, ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{}, &RequestError{Code: -32603, Message: "Read failed"}
+ },
+ RequestPermissionFunc: func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error) {
+ return RequestPermissionResponse{}, &RequestError{Code: -32603, Message: "Permission denied"}
+ },
+ SessionUpdateFunc: func(context.Context, SessionNotification) error { return nil },
+ }, c2aW, a2cR)
+ agentConn := NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(context.Context, InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{}, &RequestError{Code: -32603, Message: "Failed to initialize"}
+ },
+ NewSessionFunc: func(context.Context, NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{}, &RequestError{Code: -32603, Message: "Failed to create session"}
+ },
+ LoadSessionFunc: func(context.Context, LoadSessionRequest) (LoadSessionResponse, error) {
+ return LoadSessionResponse{}, &RequestError{Code: -32603, Message: "Failed to load session"}
+ },
+ AuthenticateFunc: func(context.Context, AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, &RequestError{Code: -32603, Message: "Authentication failed"}
+ },
+ PromptFunc: func(context.Context, PromptRequest) (PromptResponse, error) {
+ return PromptResponse{}, &RequestError{Code: -32603, Message: "Prompt failed"}
+ },
+ CancelFunc: func(context.Context, CancelNotification) error { return nil },
+ }, a2cW, c2aR)
+
+ // Client->Agent direction: expect error
+ if _, err := agentConn.WriteTextFile(ctx, WriteTextFileRequest{Path: "/test.txt", Content: "test", SessionId: "test-session"}); err == nil {
+ t.Fatalf("expected error for writeTextFile, got nil")
+ }
+
+ // Agent->Client direction: expect error
+ if _, err := c.NewSession(ctx, NewSessionRequest{Cwd: "/test", McpServers: []McpServer{}}); err == nil {
+ t.Fatalf("expected error for newSession, got nil")
+ }
+}
+
+// Test concurrent requests handling similar to TS suite
+func TestConnectionHandlesConcurrentRequests(t *testing.T) {
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ var mu sync.Mutex
+ requestCount := 0
+
+ _ = NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error) {
+ mu.Lock()
+ requestCount++
+ mu.Unlock()
+ time.Sleep(40 * time.Millisecond)
+ return WriteTextFileResponse{}, nil
+ },
+ ReadTextFileFunc: func(_ context.Context, req ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{Content: "Content of " + req.Path}, nil
+ },
+ RequestPermissionFunc: func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error) {
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{OptionId: "allow"}}}, nil
+ },
+ SessionUpdateFunc: func(context.Context, SessionNotification) error { return nil },
+ }, c2aW, a2cR)
+ agentConn := NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(context.Context, InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{ProtocolVersion: ProtocolVersionNumber, AgentCapabilities: AgentCapabilities{LoadSession: false}, AuthMethods: []AuthMethod{}}, nil
+ },
+ NewSessionFunc: func(context.Context, NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{SessionId: "test-session"}, nil
+ },
+ LoadSessionFunc: func(context.Context, LoadSessionRequest) (LoadSessionResponse, error) {
+ return LoadSessionResponse{}, nil
+ },
+ AuthenticateFunc: func(context.Context, AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, nil
+ },
+ PromptFunc: func(context.Context, PromptRequest) (PromptResponse, error) {
+ return PromptResponse{StopReason: "end_turn"}, nil
+ },
+ CancelFunc: func(context.Context, CancelNotification) error { return nil },
+ }, a2cW, c2aR)
+
+ var wg sync.WaitGroup
+ errs := make([]error, 3)
+ for i, p := range []WriteTextFileRequest{
+ {Path: "/file1.txt", Content: "content1", SessionId: "session1"},
+ {Path: "/file2.txt", Content: "content2", SessionId: "session1"},
+ {Path: "/file3.txt", Content: "content3", SessionId: "session1"},
+ } {
+ wg.Add(1)
+ idx := i
+ req := p
+ go func() {
+ defer wg.Done()
+ _, errs[idx] = agentConn.WriteTextFile(context.Background(), req)
+ }()
+ }
+ wg.Wait()
+ for i, err := range errs {
+ if err != nil {
+ t.Fatalf("request %d failed: %v", i, err)
+ }
+ }
+ mu.Lock()
+ got := requestCount
+ mu.Unlock()
+ if got != 3 {
+ t.Fatalf("expected 3 requests, got %d", got)
+ }
+}
+
+// Test message ordering
+func TestConnectionHandlesMessageOrdering(t *testing.T) {
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ var mu sync.Mutex
+ var log []string
+ push := func(s string) { mu.Lock(); defer mu.Unlock(); log = append(log, s) }
+
+ cs := NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(_ context.Context, req WriteTextFileRequest) (WriteTextFileResponse, error) {
+ push("writeTextFile called: " + req.Path)
+ return WriteTextFileResponse{}, nil
+ },
+ ReadTextFileFunc: func(_ context.Context, req ReadTextFileRequest) (ReadTextFileResponse, error) {
+ push("readTextFile called: " + req.Path)
+ return ReadTextFileResponse{Content: "test content"}, nil
+ },
+ RequestPermissionFunc: func(_ context.Context, req RequestPermissionRequest) (RequestPermissionResponse, error) {
+ title := ""
+ if req.ToolCall.Title != nil {
+ title = *req.ToolCall.Title
+ }
+ push("requestPermission called: " + title)
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{OptionId: "allow"}}}, nil
+ },
+ SessionUpdateFunc: func(context.Context, SessionNotification) error { return nil },
+ }, c2aW, a2cR)
+ as := NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(context.Context, InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{ProtocolVersion: ProtocolVersionNumber, AgentCapabilities: AgentCapabilities{LoadSession: false}, AuthMethods: []AuthMethod{}}, nil
+ },
+ NewSessionFunc: func(_ context.Context, p NewSessionRequest) (NewSessionResponse, error) {
+ push("newSession called: " + p.Cwd)
+ return NewSessionResponse{SessionId: "test-session"}, nil
+ },
+ LoadSessionFunc: func(_ context.Context, p LoadSessionRequest) (LoadSessionResponse, error) {
+ push("loadSession called: " + string(p.SessionId))
+ return LoadSessionResponse{}, nil
+ },
+ AuthenticateFunc: func(_ context.Context, p AuthenticateRequest) (AuthenticateResponse, error) {
+ push("authenticate called: " + string(p.MethodId))
+ return AuthenticateResponse{}, nil
+ },
+ PromptFunc: func(_ context.Context, p PromptRequest) (PromptResponse, error) {
+ push("prompt called: " + string(p.SessionId))
+ return PromptResponse{StopReason: "end_turn"}, nil
+ },
+ CancelFunc: func(_ context.Context, p CancelNotification) error {
+ push("cancelled called: " + string(p.SessionId))
+ return nil
+ },
+ }, a2cW, c2aR)
+
+ if _, err := cs.NewSession(context.Background(), NewSessionRequest{Cwd: "/test", McpServers: []McpServer{}}); err != nil {
+ t.Fatalf("newSession error: %v", err)
+ }
+ if _, err := as.WriteTextFile(context.Background(), WriteTextFileRequest{Path: "/test.txt", Content: "test", SessionId: "test-session"}); err != nil {
+ t.Fatalf("writeTextFile error: %v", err)
+ }
+ if _, err := as.ReadTextFile(context.Background(), ReadTextFileRequest{Path: "/test.txt", SessionId: "test-session"}); err != nil {
+ t.Fatalf("readTextFile error: %v", err)
+ }
+ if _, err := as.RequestPermission(context.Background(), RequestPermissionRequest{
+ SessionId: "test-session",
+ ToolCall: ToolCallUpdate{
+ Title: Ptr("Execute command"),
+ Kind: ptr(ToolKindExecute),
+ Status: ptr(ToolCallStatusPending),
+ ToolCallId: "tool-123",
+ Content: []ToolCallContent{ToolContent(TextBlock("ls -la"))},
+ },
+ Options: []PermissionOption{
+ {Kind: "allow_once", Name: "Allow", OptionId: "allow"},
+ {Kind: "reject_once", Name: "Reject", OptionId: "reject"},
+ },
+ }); err != nil {
+ t.Fatalf("requestPermission error: %v", err)
+ }
+
+ expected := []string{
+ "newSession called: /test",
+ "writeTextFile called: /test.txt",
+ "readTextFile called: /test.txt",
+ "requestPermission called: Execute command",
+ }
+
+ mu.Lock()
+ got := append([]string(nil), log...)
+ mu.Unlock()
+ if len(got) != len(expected) {
+ t.Fatalf("log length mismatch: got %d want %d (%v)", len(got), len(expected), got)
+ }
+ for i := range expected {
+ if got[i] != expected[i] {
+ t.Fatalf("log[%d] = %q, want %q", i, got[i], expected[i])
+ }
+ }
+}
+
+// Test notifications
+func TestConnectionHandlesNotifications(t *testing.T) {
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ var mu sync.Mutex
+ var logs []string
+ push := func(s string) { mu.Lock(); logs = append(logs, s); mu.Unlock() }
+
+ clientSide := NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error) {
+ return WriteTextFileResponse{}, nil
+ },
+ ReadTextFileFunc: func(context.Context, ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{Content: "test"}, nil
+ },
+ RequestPermissionFunc: func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error) {
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{OptionId: "allow"}}}, nil
+ },
+ SessionUpdateFunc: func(_ context.Context, n SessionNotification) error {
+ if n.Update.AgentMessageChunk != nil {
+ if n.Update.AgentMessageChunk.Content.Text != nil {
+ push("agent message: " + n.Update.AgentMessageChunk.Content.Text.Text)
+ } else {
+ // Fallback to generic message detection
+ push("agent message: Hello from agent")
+ }
+ }
+ return nil
+ },
+ }, c2aW, a2cR)
+ agentSide := NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(context.Context, InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{ProtocolVersion: ProtocolVersionNumber, AgentCapabilities: AgentCapabilities{LoadSession: false}, AuthMethods: []AuthMethod{}}, nil
+ },
+ NewSessionFunc: func(context.Context, NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{SessionId: "test-session"}, nil
+ },
+ LoadSessionFunc: func(context.Context, LoadSessionRequest) (LoadSessionResponse, error) {
+ return LoadSessionResponse{}, nil
+ },
+ AuthenticateFunc: func(context.Context, AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, nil
+ },
+ PromptFunc: func(context.Context, PromptRequest) (PromptResponse, error) {
+ return PromptResponse{StopReason: "end_turn"}, nil
+ },
+ CancelFunc: func(_ context.Context, p CancelNotification) error {
+ push("cancelled: " + string(p.SessionId))
+ return nil
+ },
+ }, a2cW, c2aR)
+
+ if err := agentSide.SessionUpdate(context.Background(), SessionNotification{
+ SessionId: "test-session",
+ Update: SessionUpdate{
+ AgentMessageChunk: &SessionUpdateAgentMessageChunk{
+ Content: TextBlock("Hello from agent"),
+ },
+ },
+ }); err != nil {
+ t.Fatalf("sessionUpdate error: %v", err)
+ }
+
+ if err := clientSide.Cancel(context.Background(), CancelNotification{SessionId: "test-session"}); err != nil {
+ t.Fatalf("cancel error: %v", err)
+ }
+
+ time.Sleep(50 * time.Millisecond)
+
+ mu.Lock()
+ got := append([]string(nil), logs...)
+ mu.Unlock()
+ want1, want2 := "agent message: Hello from agent", "cancelled: test-session"
+ if !slices.Contains(got, want1) || !slices.Contains(got, want2) {
+ t.Fatalf("notification logs mismatch: %v", got)
+ }
+}
+
+// Test initialize method behavior
+func TestConnectionHandlesInitialize(t *testing.T) {
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ agentConn := NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error) {
+ return WriteTextFileResponse{}, nil
+ },
+ ReadTextFileFunc: func(context.Context, ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{Content: "test"}, nil
+ },
+ RequestPermissionFunc: func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error) {
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{OptionId: "allow"}}}, nil
+ },
+ SessionUpdateFunc: func(context.Context, SessionNotification) error { return nil },
+ }, c2aW, a2cR)
+ _ = NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(_ context.Context, p InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{
+ ProtocolVersion: p.ProtocolVersion,
+ AgentCapabilities: AgentCapabilities{
+ LoadSession: true,
+ },
+ AuthMethods: []AuthMethod{
+ {
+ Id: "oauth",
+ Name: "OAuth",
+ Description: Ptr("Authenticate with OAuth"),
+ },
+ },
+ }, nil
+ },
+ NewSessionFunc: func(context.Context, NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{SessionId: "test-session"}, nil
+ },
+ LoadSessionFunc: func(context.Context, LoadSessionRequest) (LoadSessionResponse, error) {
+ return LoadSessionResponse{}, nil
+ },
+ AuthenticateFunc: func(context.Context, AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, nil
+ },
+ PromptFunc: func(context.Context, PromptRequest) (PromptResponse, error) {
+ return PromptResponse{StopReason: "end_turn"}, nil
+ },
+ CancelFunc: func(context.Context, CancelNotification) error { return nil },
+ }, a2cW, c2aR)
+
+ resp, err := agentConn.Initialize(context.Background(), InitializeRequest{
+ ProtocolVersion: ProtocolVersionNumber,
+ ClientCapabilities: ClientCapabilities{Fs: FileSystemCapability{ReadTextFile: false, WriteTextFile: false}},
+ })
+ if err != nil {
+ t.Fatalf("initialize error: %v", err)
+ }
+ if resp.ProtocolVersion != ProtocolVersionNumber {
+ t.Fatalf("protocol version mismatch: got %d want %d", resp.ProtocolVersion, ProtocolVersionNumber)
+ }
+ if !resp.AgentCapabilities.LoadSession {
+ t.Fatalf("expected loadSession true")
+ }
+ if len(resp.AuthMethods) != 1 || resp.AuthMethods[0].Id != "oauth" {
+ t.Fatalf("unexpected authMethods: %+v", resp.AuthMethods)
+ }
+}
+
+func ptr[T any](t T) *T {
+ return &t
+}
+
+// Test that canceling the client's Prompt context sends a session/cancel
+// to the agent, and that the connection remains usable afterwards.
+func TestPromptCancellationSendsCancelAndAllowsNewSession(t *testing.T) {
+ c2aR, c2aW := io.Pipe()
+ a2cR, a2cW := io.Pipe()
+
+ cancelCh := make(chan string, 1)
+ promptDone := make(chan struct{}, 1)
+
+ // Agent side: Prompt waits for ctx cancellation; Cancel records the sessionId
+ _ = NewAgentSideConnection(agentFuncs{
+ InitializeFunc: func(context.Context, InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{ProtocolVersion: ProtocolVersionNumber}, nil
+ },
+ NewSessionFunc: func(context.Context, NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{SessionId: "s-1"}, nil
+ },
+ LoadSessionFunc: func(context.Context, LoadSessionRequest) (LoadSessionResponse, error) {
+ return LoadSessionResponse{}, nil
+ },
+ AuthenticateFunc: func(context.Context, AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, nil
+ },
+ PromptFunc: func(ctx context.Context, p PromptRequest) (PromptResponse, error) {
+ <-ctx.Done()
+ // mark that prompt finished due to cancellation
+ select {
+ case promptDone <- struct{}{}:
+ default:
+ }
+ return PromptResponse{StopReason: StopReasonCancelled}, nil
+ },
+ CancelFunc: func(context.Context, CancelNotification) error {
+ select {
+ case cancelCh <- "s-1":
+ default:
+ }
+ return nil
+ },
+ }, a2cW, c2aR)
+
+ // Client side
+ cs := NewClientSideConnection(&clientFuncs{
+ WriteTextFileFunc: func(context.Context, WriteTextFileRequest) (WriteTextFileResponse, error) {
+ return WriteTextFileResponse{}, nil
+ },
+ ReadTextFileFunc: func(context.Context, ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{Content: ""}, nil
+ },
+ RequestPermissionFunc: func(context.Context, RequestPermissionRequest) (RequestPermissionResponse, error) {
+ return RequestPermissionResponse{}, nil
+ },
+ SessionUpdateFunc: func(context.Context, SessionNotification) error { return nil },
+ }, c2aW, a2cR)
+
+ // Initialize and create a session
+ if _, err := cs.Initialize(context.Background(), InitializeRequest{ProtocolVersion: ProtocolVersionNumber}); err != nil {
+ t.Fatalf("initialize: %v", err)
+ }
+ sess, err := cs.NewSession(context.Background(), NewSessionRequest{Cwd: "/", McpServers: []McpServer{}})
+ if err != nil {
+ t.Fatalf("newSession: %v", err)
+ }
+
+ // Start a prompt with a cancelable context, then cancel it
+ turnCtx, cancel := context.WithCancel(context.Background())
+ errCh := make(chan error, 1)
+ go func() {
+ _, err := cs.Prompt(turnCtx, PromptRequest{SessionId: sess.SessionId, Prompt: []ContentBlock{TextBlock("hello")}})
+ errCh <- err
+ }()
+
+ time.Sleep(50 * time.Millisecond)
+ cancel()
+
+ // Expect a session/cancel notification on the agent side
+ select {
+ case sid := <-cancelCh:
+ if sid != string(sess.SessionId) && sid != "s-1" { // allow either depending on agent NewSession response
+ t.Fatalf("unexpected cancel session id: %q", sid)
+ }
+ case <-time.After(1 * time.Second):
+ t.Fatalf("timeout waiting for session/cancel")
+ }
+
+ // Agent's prompt should have finished due to ctx cancellation
+ select {
+ case <-promptDone:
+ case <-time.After(1 * time.Second):
+ t.Fatalf("timeout waiting for prompt to finish after cancel")
+ }
+
+ // Connection remains usable: create another session
+ if _, err := cs.NewSession(context.Background(), NewSessionRequest{Cwd: "/", McpServers: []McpServer{}}); err != nil {
+ t.Fatalf("newSession after cancel: %v", err)
+ }
+}
diff --git a/go/agent.go b/go/agent.go
new file mode 100644
index 00000000..d561fd57
--- /dev/null
+++ b/go/agent.go
@@ -0,0 +1,33 @@
+package acp
+
+import (
+ "context"
+ "io"
+ "log/slog"
+ "sync"
+)
+
+// AgentSideConnection represents the agent's view of a connection to a client.
+type AgentSideConnection struct {
+ conn *Connection
+ agent Agent
+
+ mu sync.Mutex
+ sessionCancels map[string]context.CancelFunc
+}
+
+// NewAgentSideConnection creates a new agent-side connection bound to the
+// provided Agent implementation.
+func NewAgentSideConnection(agent Agent, peerInput io.Writer, peerOutput io.Reader) *AgentSideConnection {
+ asc := &AgentSideConnection{}
+ asc.agent = agent
+ asc.sessionCancels = make(map[string]context.CancelFunc)
+ asc.conn = NewConnection(asc.handle, peerInput, peerOutput)
+ return asc
+}
+
+// Done exposes a channel that closes when the peer disconnects.
+func (c *AgentSideConnection) Done() <-chan struct{} { return c.conn.Done() }
+
+// SetLogger directs connection diagnostics to the provided logger.
+func (c *AgentSideConnection) SetLogger(l *slog.Logger) { c.conn.SetLogger(l) }
diff --git a/go/agent_gen.go b/go/agent_gen.go
new file mode 100644
index 00000000..3dab457d
--- /dev/null
+++ b/go/agent_gen.go
@@ -0,0 +1,179 @@
+// Code generated by acp-go-generator; DO NOT EDIT.
+
+package acp
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (a *AgentSideConnection) handle(ctx context.Context, method string, params json.RawMessage) (any, *RequestError) {
+ switch method {
+ case AgentMethodAuthenticate:
+ var p AuthenticateRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := a.agent.Authenticate(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodInitialize:
+ var p InitializeRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := a.agent.Initialize(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodModelSelect:
+ var p SetSessionModelRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ exp, ok := a.agent.(AgentExperimental)
+ if !ok {
+ return nil, NewMethodNotFound(method)
+ }
+ resp, err := exp.SetSessionModel(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodSessionCancel:
+ var p CancelNotification
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ a.mu.Lock()
+ if cn, ok := a.sessionCancels[string(p.SessionId)]; ok {
+ cn()
+ delete(a.sessionCancels, string(p.SessionId))
+ }
+ a.mu.Unlock()
+ if err := a.agent.Cancel(ctx, p); err != nil {
+ return nil, toReqErr(err)
+ }
+ return nil, nil
+ case AgentMethodSessionLoad:
+ var p LoadSessionRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ loader, ok := a.agent.(AgentLoader)
+ if !ok {
+ return nil, NewMethodNotFound(method)
+ }
+ resp, err := loader.LoadSession(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodSessionNew:
+ var p NewSessionRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := a.agent.NewSession(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodSessionPrompt:
+ var p PromptRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ var reqCtx context.Context
+ var cancel context.CancelFunc
+ reqCtx, cancel = context.WithCancel(ctx)
+ a.mu.Lock()
+ if prev, ok := a.sessionCancels[string(p.SessionId)]; ok {
+ prev()
+ }
+ a.sessionCancels[string(p.SessionId)] = cancel
+ a.mu.Unlock()
+ resp, err := a.agent.Prompt(reqCtx, p)
+ a.mu.Lock()
+ delete(a.sessionCancels, string(p.SessionId))
+ a.mu.Unlock()
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case AgentMethodSessionSetMode:
+ var p SetSessionModeRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := a.agent.SetSessionMode(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ default:
+ return nil, NewMethodNotFound(method)
+ }
+}
+func (c *AgentSideConnection) ReadTextFile(ctx context.Context, params ReadTextFileRequest) (ReadTextFileResponse, error) {
+ resp, err := SendRequest[ReadTextFileResponse](c.conn, ctx, ClientMethodFsReadTextFile, params)
+ return resp, err
+}
+func (c *AgentSideConnection) WriteTextFile(ctx context.Context, params WriteTextFileRequest) (WriteTextFileResponse, error) {
+ resp, err := SendRequest[WriteTextFileResponse](c.conn, ctx, ClientMethodFsWriteTextFile, params)
+ return resp, err
+}
+func (c *AgentSideConnection) RequestPermission(ctx context.Context, params RequestPermissionRequest) (RequestPermissionResponse, error) {
+ resp, err := SendRequest[RequestPermissionResponse](c.conn, ctx, ClientMethodSessionRequestPermission, params)
+ return resp, err
+}
+func (c *AgentSideConnection) SessionUpdate(ctx context.Context, params SessionNotification) error {
+ return c.conn.SendNotification(ctx, ClientMethodSessionUpdate, params)
+}
+func (c *AgentSideConnection) CreateTerminal(ctx context.Context, params CreateTerminalRequest) (CreateTerminalResponse, error) {
+ resp, err := SendRequest[CreateTerminalResponse](c.conn, ctx, ClientMethodTerminalCreate, params)
+ return resp, err
+}
+func (c *AgentSideConnection) KillTerminalCommand(ctx context.Context, params KillTerminalCommandRequest) (KillTerminalCommandResponse, error) {
+ resp, err := SendRequest[KillTerminalCommandResponse](c.conn, ctx, ClientMethodTerminalKill, params)
+ return resp, err
+}
+func (c *AgentSideConnection) TerminalOutput(ctx context.Context, params TerminalOutputRequest) (TerminalOutputResponse, error) {
+ resp, err := SendRequest[TerminalOutputResponse](c.conn, ctx, ClientMethodTerminalOutput, params)
+ return resp, err
+}
+func (c *AgentSideConnection) ReleaseTerminal(ctx context.Context, params ReleaseTerminalRequest) (ReleaseTerminalResponse, error) {
+ resp, err := SendRequest[ReleaseTerminalResponse](c.conn, ctx, ClientMethodTerminalRelease, params)
+ return resp, err
+}
+func (c *AgentSideConnection) WaitForTerminalExit(ctx context.Context, params WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error) {
+ resp, err := SendRequest[WaitForTerminalExitResponse](c.conn, ctx, ClientMethodTerminalWaitForExit, params)
+ return resp, err
+}
diff --git a/go/client.go b/go/client.go
new file mode 100644
index 00000000..3dc04ac5
--- /dev/null
+++ b/go/client.go
@@ -0,0 +1,27 @@
+package acp
+
+import (
+ "io"
+ "log/slog"
+)
+
+// ClientSideConnection provides the client's view of the connection and implements Agent calls.
+type ClientSideConnection struct {
+ conn *Connection
+ client Client
+}
+
+// NewClientSideConnection creates a new client-side connection bound to the
+// provided Client implementation.
+func NewClientSideConnection(client Client, peerInput io.Writer, peerOutput io.Reader) *ClientSideConnection {
+ csc := &ClientSideConnection{}
+ csc.client = client
+ csc.conn = NewConnection(csc.handle, peerInput, peerOutput)
+ return csc
+}
+
+// Done exposes a channel that closes when the peer disconnects.
+func (c *ClientSideConnection) Done() <-chan struct{} { return c.conn.Done() }
+
+// SetLogger directs connection diagnostics to the provided logger.
+func (c *ClientSideConnection) SetLogger(l *slog.Logger) { c.conn.SetLogger(l) }
diff --git a/go/client_gen.go b/go/client_gen.go
new file mode 100644
index 00000000..62705cef
--- /dev/null
+++ b/go/client_gen.go
@@ -0,0 +1,167 @@
+// Code generated by acp-go-generator; DO NOT EDIT.
+
+package acp
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (c *ClientSideConnection) handle(ctx context.Context, method string, params json.RawMessage) (any, *RequestError) {
+ switch method {
+ case ClientMethodFsReadTextFile:
+ var p ReadTextFileRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.ReadTextFile(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodFsWriteTextFile:
+ var p WriteTextFileRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.WriteTextFile(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodSessionRequestPermission:
+ var p RequestPermissionRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.RequestPermission(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodSessionUpdate:
+ var p SessionNotification
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := c.client.SessionUpdate(ctx, p); err != nil {
+ return nil, toReqErr(err)
+ }
+ return nil, nil
+ case ClientMethodTerminalCreate:
+ var p CreateTerminalRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.CreateTerminal(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodTerminalKill:
+ var p KillTerminalCommandRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.KillTerminalCommand(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodTerminalOutput:
+ var p TerminalOutputRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.TerminalOutput(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodTerminalRelease:
+ var p ReleaseTerminalRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.ReleaseTerminal(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ case ClientMethodTerminalWaitForExit:
+ var p WaitForTerminalExitRequest
+ if err := json.Unmarshal(params, &p); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ if err := p.Validate(); err != nil {
+ return nil, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ resp, err := c.client.WaitForTerminalExit(ctx, p)
+ if err != nil {
+ return nil, toReqErr(err)
+ }
+ return resp, nil
+ default:
+ return nil, NewMethodNotFound(method)
+ }
+}
+func (c *ClientSideConnection) Authenticate(ctx context.Context, params AuthenticateRequest) (AuthenticateResponse, error) {
+ resp, err := SendRequest[AuthenticateResponse](c.conn, ctx, AgentMethodAuthenticate, params)
+ return resp, err
+}
+func (c *ClientSideConnection) Initialize(ctx context.Context, params InitializeRequest) (InitializeResponse, error) {
+ resp, err := SendRequest[InitializeResponse](c.conn, ctx, AgentMethodInitialize, params)
+ return resp, err
+}
+func (c *ClientSideConnection) SetSessionModel(ctx context.Context, params SetSessionModelRequest) (SetSessionModelResponse, error) {
+ resp, err := SendRequest[SetSessionModelResponse](c.conn, ctx, AgentMethodModelSelect, params)
+ return resp, err
+}
+func (c *ClientSideConnection) Cancel(ctx context.Context, params CancelNotification) error {
+ return c.conn.SendNotification(ctx, AgentMethodSessionCancel, params)
+}
+func (c *ClientSideConnection) LoadSession(ctx context.Context, params LoadSessionRequest) (LoadSessionResponse, error) {
+ resp, err := SendRequest[LoadSessionResponse](c.conn, ctx, AgentMethodSessionLoad, params)
+ return resp, err
+}
+func (c *ClientSideConnection) NewSession(ctx context.Context, params NewSessionRequest) (NewSessionResponse, error) {
+ resp, err := SendRequest[NewSessionResponse](c.conn, ctx, AgentMethodSessionNew, params)
+ return resp, err
+}
+func (c *ClientSideConnection) Prompt(ctx context.Context, params PromptRequest) (PromptResponse, error) {
+ resp, err := SendRequest[PromptResponse](c.conn, ctx, AgentMethodSessionPrompt, params)
+ if err != nil {
+ if ctx.Err() != nil {
+ _ = c.Cancel(context.Background(), CancelNotification{SessionId: params.SessionId})
+ }
+ }
+ return resp, err
+}
+func (c *ClientSideConnection) SetSessionMode(ctx context.Context, params SetSessionModeRequest) (SetSessionModeResponse, error) {
+ resp, err := SendRequest[SetSessionModeResponse](c.conn, ctx, AgentMethodSessionSetMode, params)
+ return resp, err
+}
diff --git a/go/cmd/generate/go.mod b/go/cmd/generate/go.mod
new file mode 100644
index 00000000..5bee14f6
--- /dev/null
+++ b/go/cmd/generate/go.mod
@@ -0,0 +1,5 @@
+module github.com/zed-industries/agent-client-protocol/go/cmd/generate
+
+go 1.21
+
+require github.com/dave/jennifer v1.7.1
diff --git a/go/cmd/generate/go.sum b/go/cmd/generate/go.sum
new file mode 100644
index 00000000..1a27f02d
--- /dev/null
+++ b/go/cmd/generate/go.sum
@@ -0,0 +1,2 @@
+github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
+github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
diff --git a/go/cmd/generate/internal/emit/constants.go b/go/cmd/generate/internal/emit/constants.go
new file mode 100644
index 00000000..70747729
--- /dev/null
+++ b/go/cmd/generate/internal/emit/constants.go
@@ -0,0 +1,63 @@
+package emit
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/util"
+)
+
+// WriteConstantsJen writes the version and method constants to constants_gen.go.
+func WriteConstantsJen(outDir string, meta *load.Meta) error {
+ f := NewFile("acp")
+ f.HeaderComment("Code generated by acp-go-generator; DO NOT EDIT.")
+ f.Comment("ProtocolVersionNumber is the ACP protocol version supported by this SDK.")
+ f.Const().Id("ProtocolVersionNumber").Op("=").Lit(meta.Version)
+
+ // Agent methods
+ amKeys := make([]string, 0, len(meta.AgentMethods))
+ for k := range meta.AgentMethods {
+ amKeys = append(amKeys, k)
+ }
+ sort.Strings(amKeys)
+ var agentDefs []Code
+ for _, k := range amKeys {
+ wire := meta.AgentMethods[k]
+ agentDefs = append(agentDefs, Id("AgentMethod"+toExportedConst(k)).Op("=").Lit(wire))
+ }
+ f.Comment("Agent method names")
+ f.Const().Defs(agentDefs...)
+
+ // Client methods
+ cmKeys := make([]string, 0, len(meta.ClientMethods))
+ for k := range meta.ClientMethods {
+ cmKeys = append(cmKeys, k)
+ }
+ sort.Strings(cmKeys)
+ var clientDefs []Code
+ for _, k := range cmKeys {
+ wire := meta.ClientMethods[k]
+ clientDefs = append(clientDefs, Id("ClientMethod"+toExportedConst(k)).Op("=").Lit(wire))
+ }
+ f.Comment("Client method names")
+ f.Const().Defs(clientDefs...)
+
+ var buf bytes.Buffer
+ if err := f.Render(&buf); err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(outDir, "constants_gen.go"), buf.Bytes(), 0o644)
+}
+
+// Helpers kept private to this package (copy from original main)
+func toExportedConst(s string) string {
+ parts := strings.Split(s, "_")
+ for i := range parts {
+ parts[i] = util.TitleWord(parts[i])
+ }
+ return strings.Join(parts, "")
+}
diff --git a/go/cmd/generate/internal/emit/dispatch.go b/go/cmd/generate/internal/emit/dispatch.go
new file mode 100644
index 00000000..9c06ca92
--- /dev/null
+++ b/go/cmd/generate/internal/emit/dispatch.go
@@ -0,0 +1,268 @@
+package emit
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/ir"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+)
+
+// WriteDispatchJen emits agent_gen.go and client_gen.go with handlers and wrappers.
+func WriteDispatchJen(outDir string, schema *load.Schema, meta *load.Meta) error {
+ groups := ir.BuildMethodGroups(schema, meta)
+
+ // Agent handler + outbound wrappers
+ fAgent := NewFile("acp")
+ fAgent.HeaderComment("Code generated by acp-go-generator; DO NOT EDIT.")
+
+ amKeys := make([]string, 0, len(meta.AgentMethods))
+ for k := range meta.AgentMethods {
+ amKeys = append(amKeys, k)
+ }
+ sort.Strings(amKeys)
+ switchCases := []Code{}
+ for _, k := range amKeys {
+ wire := meta.AgentMethods[k]
+ mi := groups["agent|"+wire]
+ if mi == nil {
+ continue
+ }
+ caseBody := []Code{}
+ if mi.Notif != "" {
+ caseBody = append(caseBody, jUnmarshalValidate(mi.Notif)...)
+ // Special-case: session/cancel should also cancel any in-flight prompt ctx for the session.
+ if mi.Method == "session/cancel" {
+ caseBody = append(caseBody,
+ // cancel active prompt context if present
+ Id("a").Dot("mu").Dot("Lock").Call(),
+ If(List(Id("cn"), Id("ok")).Op(":=").Id("a").Dot("sessionCancels").Index(Id("string").Call(Id("p").Dot("SessionId"))), Id("ok")).Block(
+ Id("cn").Call(),
+ Id("delete").Call(Id("a").Dot("sessionCancels"), Id("string").Call(Id("p").Dot("SessionId"))),
+ ),
+ Id("a").Dot("mu").Dot("Unlock").Call(),
+ )
+ }
+ callName := ir.DispatchMethodNameForNotification(k, mi.Notif)
+ caseBody = append(caseBody, jCallNotification("a.agent", callName)...)
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ caseBody = append(caseBody, jUnmarshalValidate(mi.Req)...)
+ methodName := strings.TrimSuffix(mi.Req, "Request")
+ pre, recv := jAgentAssert(mi.Binding)
+ if pre != nil {
+ caseBody = append(caseBody, pre...)
+ }
+ if mi.Method == "session/prompt" {
+ // Derive a cancellable context per session prompt.
+ caseBody = append(caseBody,
+ Var().Id("reqCtx").Qual("context", "Context"), Var().Id("cancel").Qual("context", "CancelFunc"),
+ List(Id("reqCtx"), Id("cancel")).Op("=").Qual("context", "WithCancel").Call(Id("ctx")),
+ Id("a").Dot("mu").Dot("Lock").Call(),
+ If(List(Id("prev"), Id("ok")).Op(":=").Id("a").Dot("sessionCancels").Index(Id("string").Call(Id("p").Dot("SessionId"))), Id("ok")).Block(Id("prev").Call()),
+ Id("a").Dot("sessionCancels").Index(Id("string").Call(Id("p").Dot("SessionId"))).Op("=").Id("cancel"),
+ Id("a").Dot("mu").Dot("Unlock").Call(),
+ )
+ // Call agent.Prompt(reqCtx, p)
+ caseBody = append(caseBody,
+ List(Id("resp"), Id("err")).Op(":=").Id(recv).Dot(methodName).Call(Id("reqCtx"), Id("p")),
+ // cleanup entry after return
+ Id("a").Dot("mu").Dot("Lock").Call(),
+ Id("delete").Call(Id("a").Dot("sessionCancels"), Id("string").Call(Id("p").Dot("SessionId"))),
+ Id("a").Dot("mu").Dot("Unlock").Call(),
+ If(Id("err").Op("!=").Nil()).Block(jRetToReqErr()),
+ Return(Id("resp"), Nil()),
+ )
+ } else if ir.IsNullResponse(schema.Defs[respName]) {
+ caseBody = append(caseBody, jCallRequestNoResp(recv, methodName)...)
+ } else {
+ caseBody = append(caseBody, jCallRequestWithResp(recv, methodName)...)
+ }
+ }
+ if len(caseBody) > 0 {
+ switchCases = append(switchCases, Case(Id("AgentMethod"+toExportedConst(k))).Block(caseBody...))
+ }
+ }
+ switchCases = append(switchCases, Default().Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))))
+ fAgent.Func().Params(Id("a").Op("*").Id("AgentSideConnection")).Id("handle").Params(
+ Id("ctx").Qual("context", "Context"), Id("method").String(), Id("params").Qual("encoding/json", "RawMessage")).
+ Params(Any(), Op("*").Id("RequestError")).
+ Block(Switch(Id("method")).Block(switchCases...))
+
+ // Agent outbound wrappers (agent -> client)
+ agentConst := map[string]string{}
+ for k, v := range meta.AgentMethods {
+ agentConst[v] = "AgentMethod" + toExportedConst(k)
+ }
+ clientConst := map[string]string{}
+ for k, v := range meta.ClientMethods {
+ clientConst[v] = "ClientMethod" + toExportedConst(k)
+ }
+
+ cmKeys2 := make([]string, 0, len(meta.ClientMethods))
+ for k := range meta.ClientMethods {
+ cmKeys2 = append(cmKeys2, k)
+ }
+ sort.Strings(cmKeys2)
+ for _, k := range cmKeys2 {
+ wire := meta.ClientMethods[k]
+ mi := groups["client|"+wire]
+ if mi == nil {
+ continue
+ }
+ constName := clientConst[mi.Method]
+ if constName == "" {
+ continue
+ }
+ if mi.Notif != "" {
+ name := strings.TrimSuffix(mi.Notif, "Notification")
+ switch mi.Method {
+ case "session/update":
+ name = "SessionUpdate"
+ case "session/cancel":
+ name = "Cancel"
+ }
+ fAgent.Func().Params(Id("c").Op("*").Id("AgentSideConnection")).Id(name).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Notif)).Error().
+ Block(Return(Id("c").Dot("conn").Dot("SendNotification").Call(Id("ctx"), Id(constName), Id("params"))))
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ if ir.IsNullResponse(schema.Defs[respName]) {
+ fAgent.Func().Params(Id("c").Op("*").Id("AgentSideConnection")).Id(strings.TrimSuffix(mi.Req, "Request")).
+ Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Error().
+ Block(Return(Id("c").Dot("conn").Dot("SendRequestNoResult").Call(Id("ctx"), Id(constName), Id("params"))))
+ } else {
+ fAgent.Func().Params(Id("c").Op("*").Id("AgentSideConnection")).Id(strings.TrimSuffix(mi.Req, "Request")).
+ Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Params(Id(respName), Error()).
+ Block(
+ List(Id("resp"), Id("err")).Op(":=").Id("SendRequest").Types(Id(respName)).Call(Id("c").Dot("conn"), Id("ctx"), Id(constName), Id("params")),
+ Return(Id("resp"), Id("err")),
+ )
+ }
+ }
+ }
+ var bufA bytes.Buffer
+ if err := fAgent.Render(&bufA); err != nil {
+ return err
+ }
+ if err := os.WriteFile(filepath.Join(outDir, "agent_gen.go"), bufA.Bytes(), 0o644); err != nil {
+ return err
+ }
+
+ // Client handler + outbound wrappers
+ fClient := NewFile("acp")
+ fClient.HeaderComment("Code generated by acp-go-generator; DO NOT EDIT.")
+ cmKeys := make([]string, 0, len(meta.ClientMethods))
+ for k := range meta.ClientMethods {
+ cmKeys = append(cmKeys, k)
+ }
+ sort.Strings(cmKeys)
+ cCases := []Code{}
+ for _, k := range cmKeys {
+ wire := meta.ClientMethods[k]
+ mi := groups["client|"+wire]
+ if mi == nil {
+ continue
+ }
+ body := []Code{}
+ if mi.Notif != "" {
+ body = append(body, jUnmarshalValidate(mi.Notif)...)
+ pre, recv := jClientAssert(mi.Binding)
+ if pre != nil {
+ body = append(body, pre...)
+ }
+ callName := ir.DispatchMethodNameForNotification(k, mi.Notif)
+ body = append(body, jCallNotification(recv, callName)...)
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ body = append(body, jUnmarshalValidate(mi.Req)...)
+ methodName := strings.TrimSuffix(mi.Req, "Request")
+ pre, recv := jClientAssert(mi.Binding)
+ if pre != nil {
+ body = append(body, pre...)
+ }
+ if ir.IsNullResponse(schema.Defs[respName]) {
+ body = append(body, jCallRequestNoResp(recv, methodName)...)
+ } else {
+ body = append(body, jCallRequestWithResp(recv, methodName)...)
+ }
+ }
+ if len(body) > 0 {
+ cCases = append(cCases, Case(Id("ClientMethod"+toExportedConst(k))).Block(body...))
+ }
+ }
+ cCases = append(cCases, Default().Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))))
+ fClient.Func().Params(Id("c").Op("*").Id("ClientSideConnection")).Id("handle").Params(
+ Id("ctx").Qual("context", "Context"), Id("method").String(), Id("params").Qual("encoding/json", "RawMessage")).
+ Params(Any(), Op("*").Id("RequestError")).
+ Block(Switch(Id("method")).Block(cCases...))
+
+ // Client outbound wrappers (client -> agent)
+ amKeys2 := make([]string, 0, len(meta.AgentMethods))
+ for k := range meta.AgentMethods {
+ amKeys2 = append(amKeys2, k)
+ }
+ sort.Strings(amKeys2)
+ for _, k := range amKeys2 {
+ wire := meta.AgentMethods[k]
+ mi := groups["agent|"+wire]
+ if mi == nil {
+ continue
+ }
+ constName := agentConst[mi.Method]
+ if constName == "" {
+ continue
+ }
+ if mi.Notif != "" {
+ name := strings.TrimSuffix(mi.Notif, "Notification")
+ switch mi.Method {
+ case "session/update":
+ name = "SessionUpdate"
+ case "session/cancel":
+ name = "Cancel"
+ }
+ fClient.Func().Params(Id("c").Op("*").Id("ClientSideConnection")).Id(name).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Notif)).Error().
+ Block(Return(Id("c").Dot("conn").Dot("SendNotification").Call(Id("ctx"), Id(constName), Id("params"))))
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ if ir.IsNullResponse(schema.Defs[respName]) {
+ fClient.Func().Params(Id("c").Op("*").Id("ClientSideConnection")).Id(strings.TrimSuffix(mi.Req, "Request")).
+ Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Error().
+ Block(Return(Id("c").Dot("conn").Dot("SendRequestNoResult").Call(Id("ctx"), Id(constName), Id("params"))))
+ } else {
+ // Special-case: session/prompt — if ctx was canceled, send session/cancel best-effort.
+ if mi.Method == "session/prompt" {
+ fClient.Func().Params(Id("c").Op("*").Id("ClientSideConnection")).Id(strings.TrimSuffix(mi.Req, "Request")).
+ Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Params(Id(respName), Error()).
+ Block(
+ List(Id("resp"), Id("err")).Op(":=").Id("SendRequest").Types(Id(respName)).Call(Id("c").Dot("conn"), Id("ctx"), Id(constName), Id("params")),
+ If(Id("err").Op("!=").Nil()).Block(
+ If(Id("ctx").Dot("Err").Call().Op("!=").Nil()).Block(
+ Id("_ ").Op("=").Id("c").Dot("Cancel").Call(Qual("context", "Background").Call(), Id("CancelNotification").Values(Dict{Id("SessionId"): Id("params").Dot("SessionId")})),
+ ),
+ ),
+ Return(Id("resp"), Id("err")),
+ )
+ } else {
+ fClient.Func().Params(Id("c").Op("*").Id("ClientSideConnection")).Id(strings.TrimSuffix(mi.Req, "Request")).
+ Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Params(Id(respName), Error()).
+ Block(
+ List(Id("resp"), Id("err")).Op(":=").Id("SendRequest").Types(Id(respName)).Call(Id("c").Dot("conn"), Id("ctx"), Id(constName), Id("params")),
+ Return(Id("resp"), Id("err")),
+ )
+ }
+ }
+ }
+ }
+ var bufC bytes.Buffer
+ if err := fClient.Render(&bufC); err != nil {
+ return err
+ }
+ if err := os.WriteFile(filepath.Join(outDir, "client_gen.go"), bufC.Bytes(), 0o644); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/go/cmd/generate/internal/emit/dispatch_helpers.go b/go/cmd/generate/internal/emit/dispatch_helpers.go
new file mode 100644
index 00000000..274e762c
--- /dev/null
+++ b/go/cmd/generate/internal/emit/dispatch_helpers.go
@@ -0,0 +1,83 @@
+package emit
+
+import (
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/ir"
+)
+
+// invInvalid: return invalid params with compact json-like message
+func jInvInvalid() Code {
+ return Return(Nil(), Id("NewInvalidParams").Call(Map(String()).Any().Values(Dict{Lit("error"): Id("err").Dot("Error").Call()})))
+}
+
+// retToReqErr: wrap error to JSON-RPC request error
+func jRetToReqErr() Code { return Return(Nil(), Id("toReqErr").Call(Id("err"))) }
+
+// jUnmarshalValidate emits var p T; json.Unmarshal; p.Validate
+func jUnmarshalValidate(typeName string) []Code {
+ return []Code{
+ Var().Id("p").Id(typeName),
+ If(List(Id("err")).Op(":=").Qual("encoding/json", "Unmarshal").Call(Id("params"), Op("&").Id("p")), Id("err").Op("!=").Nil()).
+ Block(jInvInvalid()),
+ If(List(Id("err")).Op(":=").Id("p").Dot("Validate").Call(), Id("err").Op("!=").Nil()).
+ Block(jInvInvalid()),
+ }
+}
+
+// jAgentAssert returns prelude for interface assertions and the receiver name.
+func jAgentAssert(binding ir.MethodBinding) ([]Code, string) {
+ switch binding {
+ case ir.BindAgentLoader:
+ return []Code{
+ List(Id("loader"), Id("ok")).Op(":=").Id("a").Dot("agent").Assert(Id("AgentLoader")),
+ If(Op("!").Id("ok")).Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))),
+ }, "loader"
+ case ir.BindAgentExperimental:
+ return []Code{
+ List(Id("exp"), Id("ok")).Op(":=").Id("a").Dot("agent").Assert(Id("AgentExperimental")),
+ If(Op("!").Id("ok")).Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))),
+ }, "exp"
+ default:
+ return nil, "a.agent"
+ }
+}
+
+// jClientAssert returns prelude for interface assertions and the receiver name.
+func jClientAssert(binding ir.MethodBinding) ([]Code, string) {
+ switch binding {
+ case ir.BindClientExperimental:
+ return []Code{
+ List(Id("exp"), Id("ok")).Op(":=").Id("c").Dot("client").Assert(Id("ClientExperimental")),
+ If(Op("!").Id("ok")).Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))),
+ }, "exp"
+ case ir.BindClientTerminal:
+ return []Code{
+ List(Id("t"), Id("ok")).Op(":=").Id("c").Dot("client").Assert(Id("ClientTerminal")),
+ If(Op("!").Id("ok")).Block(Return(Nil(), Id("NewMethodNotFound").Call(Id("method")))),
+ }, "t"
+ default:
+ return nil, "c.client"
+ }
+}
+
+// Request call emitters for handlers
+func jCallRequestNoResp(recv, methodName string) []Code {
+ return []Code{
+ If(List(Id("err")).Op(":=").Id(recv).Dot(methodName).Call(Id("ctx"), Id("p")), Id("err").Op("!=").Nil()).Block(jRetToReqErr()),
+ Return(Nil(), Nil()),
+ }
+}
+
+func jCallRequestWithResp(recv, methodName string) []Code {
+ return []Code{
+ List(Id("resp"), Id("err")).Op(":=").Id(recv).Dot(methodName).Call(Id("ctx"), Id("p")),
+ If(Id("err").Op("!=").Nil()).Block(jRetToReqErr()),
+ Return(Id("resp"), Nil()),
+ }
+}
+
+func jCallNotification(recv, methodName string) []Code {
+ return []Code{
+ If(List(Id("err")).Op(":=").Id(recv).Dot(methodName).Call(Id("ctx"), Id("p")), Id("err").Op("!=").Nil()).Block(jRetToReqErr()),
+ Return(Nil(), Nil()),
+ }
+}
diff --git a/go/cmd/generate/internal/emit/doc.go b/go/cmd/generate/internal/emit/doc.go
new file mode 100644
index 00000000..b68cdf36
--- /dev/null
+++ b/go/cmd/generate/internal/emit/doc.go
@@ -0,0 +1,4 @@
+// Package emit contains helpers to generate the Go SDK from the
+// intermediate representation produced by the ACP generator. It
+// encapsulates code emission for types, helpers, and dispatch logic.
+package emit
diff --git a/go/cmd/generate/internal/emit/helpers.go b/go/cmd/generate/internal/emit/helpers.go
new file mode 100644
index 00000000..2f5ce5ae
--- /dev/null
+++ b/go/cmd/generate/internal/emit/helpers.go
@@ -0,0 +1,145 @@
+package emit
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/util"
+)
+
+// WriteHelpersJen emits go/helpers_gen.go with small constructor helpers
+// for common union variants and a Ptr generic helper.
+func WriteHelpersJen(outDir string, schema *load.Schema, _ *load.Meta) error {
+ f := NewFile("acp")
+ f.HeaderComment("Code generated by acp-go-generator; DO NOT EDIT.")
+
+ // Schema-driven generic helpers: New(required fields only)
+ // Iterate definitions deterministically
+ keys := make([]string, 0, len(schema.Defs))
+ for k := range schema.Defs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, name := range keys {
+ def := schema.Defs[name]
+ if def == nil || def.DocsIgnore || len(def.OneOf) == 0 {
+ continue
+ }
+ // Skip string-const unions
+ if isStringConstUnion(def) {
+ continue
+ }
+ // Skip generating New... helpers for unions that have stable, static helpers
+ // implemented in go/helpers.go.
+ switch name {
+ case "ContentBlock", "ToolCallContent", "SessionUpdate":
+ continue
+ }
+ // Build variant info similarly to types emitter
+ type vinfo struct {
+ fieldName string
+ typeName string
+ discKey string
+ discValue string
+ required []string
+ props map[string]*load.Definition
+ }
+ discKey := ""
+ for _, v := range def.OneOf {
+ if v == nil {
+ continue
+ }
+ for k, pd := range v.Properties {
+ if pd != nil && pd.Const != nil {
+ discKey = k
+ break
+ }
+ }
+ if discKey != "" {
+ break
+ }
+ }
+ variants := []vinfo{}
+ for idx, v := range def.OneOf {
+ if v == nil {
+ continue
+ }
+ // compute type name per types emitter
+ tname := v.Title
+ if tname == "" {
+ if v.Ref != "" && strings.HasPrefix(v.Ref, "#/$defs/") {
+ tname = v.Ref[len("#/$defs/"):]
+ } else {
+ if discKey != "" {
+ if pd := v.Properties[discKey]; pd != nil && pd.Const != nil {
+ s := fmt.Sprint(pd.Const)
+ tname = name + util.ToExportedField(s)
+ }
+ }
+ if tname == "" {
+ tname = name + fmt.Sprintf("Variant%d", idx+1)
+ }
+ }
+ }
+ fieldName := tname
+ dv := ""
+ if discKey != "" {
+ if pd := v.Properties[discKey]; pd != nil && pd.Const != nil {
+ s := fmt.Sprint(pd.Const)
+ fieldName = util.ToExportedField(s)
+ dv = s
+ }
+ }
+ // collect required
+ req := make([]string, len(v.Required))
+ copy(req, v.Required)
+ variants = append(variants, vinfo{fieldName: fieldName, typeName: tname, discKey: discKey, discValue: dv, required: req, props: v.Properties})
+ }
+ // Emit helper per variant: func New(...)
+ for _, vi := range variants {
+ // params: all required props except const discriminator
+ params := []Code{}
+ assigns := Dict{}
+ for _, rk := range vi.required {
+ if rk == vi.discKey {
+ continue
+ }
+ pd := vi.props[rk]
+ if pd == nil {
+ continue
+ }
+ // build param using lower-cased name
+ pname := rk
+ // field id for struct literal
+ field := util.ToExportedField(rk)
+ params = append(params, Id(pname).Add(jenTypeFor(pd)))
+ assigns[Id(field)] = Id(pname)
+ }
+ // include const discriminant if present and field exists on struct
+ if vi.discKey != "" && vi.discValue != "" {
+ assigns[Id(util.ToExportedField(vi.discKey))] = Lit(vi.discValue)
+ }
+ // Construct variant literal and wrap
+ f.Comment(fmt.Sprintf("New%s%s constructs a %s using the '%s' variant.", name, vi.fieldName, name, vi.discValue))
+ f.Func().Id("New" + name + vi.fieldName).Params(params...).Id(name).Block(
+ Return(
+ Id(name).Values(Dict{
+ Id(vi.fieldName): Op("&").Id(vi.typeName).Values(assigns),
+ }),
+ ),
+ )
+ f.Line()
+ }
+ }
+
+ var buf bytes.Buffer
+ if err := f.Render(&buf); err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(outDir, "helpers_gen.go"), buf.Bytes(), 0o644)
+}
diff --git a/go/cmd/generate/internal/emit/jenwrap.go b/go/cmd/generate/internal/emit/jenwrap.go
new file mode 100644
index 00000000..07e80855
--- /dev/null
+++ b/go/cmd/generate/internal/emit/jenwrap.go
@@ -0,0 +1,40 @@
+package emit
+
+import jen "github.com/dave/jennifer/jen"
+
+// Local aliases to avoid dot-importing jennifer while keeping concise calls.
+type (
+ Code = jen.Code
+ Dict = jen.Dict
+ Group = jen.Group
+ File = jen.File
+)
+
+var (
+ NewFile = jen.NewFile
+ Id = jen.Id
+ Lit = jen.Lit
+ Line = jen.Line
+ Func = jen.Func
+ For = jen.For
+ Range = jen.Range
+ Return = jen.Return
+ Nil = jen.Nil
+ String = jen.String
+ Int = jen.Int
+ Float64 = jen.Float64
+ Bool = jen.Bool
+ Any = jen.Any
+ Map = jen.Map
+ Index = jen.Index
+ Qual = jen.Qual
+ Error = jen.Error
+ Case = jen.Case
+ Default = jen.Default
+ Switch = jen.Switch
+ Var = jen.Var
+ If = jen.If
+ List = jen.List
+ Op = jen.Op
+ Comment = jen.Comment
+)
diff --git a/go/cmd/generate/internal/emit/types.go b/go/cmd/generate/internal/emit/types.go
new file mode 100644
index 00000000..158052ee
--- /dev/null
+++ b/go/cmd/generate/internal/emit/types.go
@@ -0,0 +1,837 @@
+package emit
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+ "sort"
+ "strings"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/ir"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/util"
+)
+
+// WriteTypesJen emits go/types_gen.go with all types and the Agent/Client interfaces.
+func WriteTypesJen(outDir string, schema *load.Schema, meta *load.Meta) error {
+ f := NewFile("acp")
+ f.HeaderComment("Code generated by acp-go-generator; DO NOT EDIT.")
+
+ // Deterministic order
+ keys := make([]string, 0, len(schema.Defs))
+ for k := range schema.Defs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ for _, name := range keys {
+ def := schema.Defs[name]
+ if def == nil {
+ continue
+ }
+
+ if def.Description != "" {
+ f.Comment(util.SanitizeComment(def.Description))
+ }
+
+ switch {
+ case len(def.Enum) > 0:
+ f.Type().Id(name).String()
+ defs := []Code{}
+ for _, v := range def.Enum {
+ s := fmt.Sprint(v)
+ defs = append(defs, Id(util.ToEnumConst(name, s)).Id(name).Op("=").Lit(s))
+ }
+ if len(defs) > 0 {
+ f.Const().Defs(defs...)
+ }
+ f.Line()
+ case isStringConstUnion(def):
+ f.Type().Id(name).String()
+ defs := []Code{}
+ for _, v := range def.OneOf {
+ if v != nil && v.Const != nil {
+ s := fmt.Sprint(v.Const)
+ defs = append(defs, Id(util.ToEnumConst(name, s)).Id(name).Op("=").Lit(s))
+ }
+ }
+ if len(defs) > 0 {
+ f.Const().Defs(defs...)
+ }
+ f.Line()
+ case len(def.AnyOf) > 0:
+ emitUnion(f, name, def.AnyOf, false)
+ case len(def.OneOf) > 0 && !isStringConstUnion(def):
+ // Generic union generation for non-enum oneOf
+ // Use the same implementation, but require exactly one variant
+ emitUnion(f, name, def.OneOf, true)
+ case ir.PrimaryType(def) == "object" && len(def.Properties) > 0:
+ st := []Code{}
+ req := map[string]struct{}{}
+ for _, r := range def.Required {
+ req[r] = struct{}{}
+ }
+ pkeys := make([]string, 0, len(def.Properties))
+ for pk := range def.Properties {
+ pkeys = append(pkeys, pk)
+ }
+ sort.Strings(pkeys)
+ // Track fields with schema defaults for generic (de)serialization
+ type DefaultKind int
+ const (
+ KindNone DefaultKind = iota
+ KindScalar
+ KindArray
+ KindObject
+ )
+ type defaultProp struct {
+ fieldName string
+ propName string
+ defaultJSON string
+ kind DefaultKind
+ allowNull bool
+ nilable bool // whether zero-value is nil (slice/map)
+ }
+ defaults := []defaultProp{}
+
+ for _, pk := range pkeys {
+ prop := def.Properties[pk]
+ field := util.ToExportedField(pk)
+ if prop.Description != "" {
+ st = append(st, Comment(util.SanitizeComment(prop.Description)))
+ }
+ tag := pk
+ // Detect defaults generically
+ var dp *defaultProp
+ if prop.Default != nil {
+ // Compute kind from default value
+ k := defaultKindOf(prop.Default)
+ // Whether field zero is nil (slice/map) for Marshal fill-in
+ nilable := ir.PrimaryType(prop) == "array" || (ir.PrimaryType(prop) == "object" && len(prop.Properties) == 0 && prop.Ref == "")
+ // Capture canonical JSON of default
+ defJSON := "null"
+ if b, err := json.Marshal(prop.Default); err == nil {
+ defJSON = string(b)
+ }
+ dp = &defaultProp{
+ fieldName: field,
+ propName: pk,
+ defaultJSON: defJSON,
+ kind: DefaultKind(k),
+ allowNull: includesNull(prop),
+ nilable: nilable,
+ }
+ defaults = append(defaults, *dp)
+ }
+ if _, ok := req[pk]; !ok {
+ // Default: omit if empty for optional fields.
+ // Keep always-present behavior only for defaults where the zero value is nil (slice/map).
+ // For typed object defaults (non-nilable), still allow omission on the wire.
+ if dp == nil || (dp.kind != KindArray && dp.kind != KindObject) || (dp != nil && !dp.nilable) {
+ tag = pk + ",omitempty"
+ }
+ }
+ // Emit an additional comment line indicating the default, if any.
+ if dp != nil && dp.defaultJSON != "null" {
+ // Insert an empty comment line before default comment (visual separator)
+ if prop.Description != "" {
+ st = append(st, Comment(""))
+ }
+ st = append(st, Comment(util.SanitizeComment(fmt.Sprintf("Defaults to %s if unset.", dp.defaultJSON))))
+ }
+ st = append(st, Id(field).Add(jenTypeForOptional(prop)).Tag(map[string]string{"json": tag}))
+ }
+ f.Type().Id(name).Struct(st...)
+ f.Line()
+
+ // If the struct has any fields with schema defaults, synthesize MarshalJSON and UnmarshalJSON
+ if len(defaults) > 0 {
+ // MarshalJSON: coerce nil slices to empty slices before encoding
+ f.Func().Params(Id("v").Id(name)).Id("MarshalJSON").Params().Params(Index().Byte(), Error()).BlockFunc(func(g *Group) {
+ g.Type().Id("Alias").Id(name)
+ g.Var().Id("a").Id("Alias")
+ g.Id("a").Op("=").Id("Alias").Call(Id("v"))
+ for _, dp := range defaults {
+ // For array/map defaults: if zero is nil, fill with default JSON when nil
+ if dp.kind == KindArray || dp.kind == KindObject {
+ if dp.nilable {
+ g.If(Id("a").Dot(dp.fieldName).Op("==").Nil()).Block(
+ Qual("encoding/json", "Unmarshal").Call(Index().Byte().Parens(Lit(dp.defaultJSON)), Op("&").Id("a").Dot(dp.fieldName)),
+ )
+ }
+ }
+ // For typed object defaults (non-nilable), we keep Option A: do not inject values on encode.
+ }
+ g.Return(Qual("encoding/json", "Marshal").Call(Id("a")))
+ })
+ f.Line()
+
+ // UnmarshalJSON: apply defaults when field is missing or null (and schema doesn't include null)
+ f.Func().Params(Id("v").Op("*").Id(name)).Id("UnmarshalJSON").Params(Id("b").Index().Byte()).Error().BlockFunc(func(g *Group) {
+ g.Var().Id("m").Map(String()).Qual("encoding/json", "RawMessage")
+ g.If(List(Id("err")).Op(":=").Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("m")), Id("err").Op("!=").Nil()).Block(Return(Id("err")))
+ g.Type().Id("Alias").Id(name)
+ g.Var().Id("a").Id("Alias")
+ g.If(List(Id("err")).Op(":=").Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("a")), Id("err").Op("!=").Nil()).Block(Return(Id("err")))
+ for _, dp := range defaults {
+ g.BlockFunc(func(h *Group) {
+ h.List(Id("_rm"), Id("_ok")).Op(":=").Id("m").Index(Lit(dp.propName))
+ // Apply default when missing, or when null and null is not allowed
+ if dp.allowNull {
+ h.If(Op("!").Id("_ok")).Block(
+ Qual("encoding/json", "Unmarshal").Call(Index().Byte().Parens(Lit(dp.defaultJSON)), Op("&").Id("a").Dot(dp.fieldName)),
+ )
+ } else {
+ h.If(Op("!").Id("_ok").Op("||").Parens(Id("string").Call(Id("_rm")).Op("==").Lit("null"))).Block(
+ Qual("encoding/json", "Unmarshal").Call(Index().Byte().Parens(Lit(dp.defaultJSON)), Op("&").Id("a").Dot(dp.fieldName)),
+ )
+ }
+ })
+ }
+ g.Op("*").Id("v").Op("=").Id(name).Call(Id("a"))
+ g.Return(Nil())
+ })
+ f.Line()
+ }
+ case ir.PrimaryType(def) == "string" || ir.PrimaryType(def) == "integer" || ir.PrimaryType(def) == "number" || ir.PrimaryType(def) == "boolean":
+ f.Type().Id(name).Add(primitiveJenType(ir.PrimaryType(def)))
+ f.Line()
+ case ir.PrimaryType(def) == "object" && len(def.Properties) == 0:
+ // Empty object shape: emit a concrete empty struct so methods can be defined
+ // and the wire encoding is consistently {} rather than null.
+ f.Type().Id(name).Struct()
+ f.Line()
+ default:
+ f.Comment(fmt.Sprintf("%s is a union or complex schema; represented generically.", name))
+ f.Type().Id(name).Any()
+ f.Line()
+ }
+
+ // validators for selected types
+ // Note: oneOf union wrappers get a generic Validate emitted in emitUnion.
+ if strings.HasSuffix(name, "Request") || strings.HasSuffix(name, "Response") || strings.HasSuffix(name, "Notification") || name == "ToolCallUpdate" {
+ emitValidateJen(f, name, def)
+ }
+ }
+
+ // Append Agent & Client interfaces from method groups
+ groups := ir.BuildMethodGroups(schema, meta)
+
+ // Agent
+ agentMethods := []Code{}
+ agentLoaderMethods := []Code{}
+ agentExperimentalMethods := []Code{}
+ for _, k := range ir.SortedKeys(meta.AgentMethods) {
+ wire := meta.AgentMethods[k]
+ mi := groups["agent|"+wire]
+ if mi == nil {
+ continue
+ }
+ target := &agentMethods
+ switch mi.Binding {
+ case ir.BindAgentLoader:
+ target = &agentLoaderMethods
+ case ir.BindAgentExperimental:
+ target = &agentExperimentalMethods
+ }
+ if mi.Notif != "" {
+ name := ir.DispatchMethodNameForNotification(k, mi.Notif)
+ *target = append(*target, Id(name).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Notif)).Error())
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ methodName := strings.TrimSuffix(mi.Req, "Request")
+ if ir.IsNullResponse(schema.Defs[respName]) {
+ *target = append(*target, Id(methodName).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Error())
+ } else {
+ *target = append(*target, Id(methodName).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Params(Id(respName), Error()))
+ }
+ }
+ }
+ f.Type().Id("Agent").Interface(agentMethods...)
+ if len(agentLoaderMethods) > 0 {
+ f.Comment("AgentLoader defines optional support for loading sessions. Implement and advertise the capability to enable 'session/load'.")
+ f.Type().Id("AgentLoader").Interface(agentLoaderMethods...)
+ }
+ // Always emit AgentExperimental, even if empty.
+ f.Comment("AgentExperimental defines unstable methods that are not part of the official spec. These may change or be removed without notice.")
+ f.Type().Id("AgentExperimental").Interface(agentExperimentalMethods...)
+
+ // Client
+ clientStable := []Code{}
+ clientExperimental := []Code{}
+ clientTerminal := []Code{}
+ for _, k := range ir.SortedKeys(meta.ClientMethods) {
+ wire := meta.ClientMethods[k]
+ mi := groups["client|"+wire]
+ if mi == nil {
+ continue
+ }
+ target := &clientStable
+ switch mi.Binding {
+ case ir.BindClientExperimental:
+ target = &clientExperimental
+ case ir.BindClientTerminal:
+ target = &clientTerminal
+ }
+ if mi.Notif != "" {
+ name := ir.DispatchMethodNameForNotification(k, mi.Notif)
+ *target = append(*target, Id(name).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Notif)).Error())
+ } else if mi.Req != "" {
+ respName := strings.TrimSuffix(mi.Req, "Request") + "Response"
+ methodName := strings.TrimSuffix(mi.Req, "Request")
+ if ir.IsNullResponse(schema.Defs[respName]) {
+ *target = append(*target, Id(methodName).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Error())
+ } else {
+ *target = append(*target, Id(methodName).Params(Id("ctx").Qual("context", "Context"), Id("params").Id(mi.Req)).Params(Id(respName), Error()))
+ }
+ }
+ }
+ f.Type().Id("Client").Interface(clientStable...)
+ if len(clientTerminal) > 0 {
+ f.Comment("ClientTerminal defines terminal-related experimental methods (x-docs-ignore). Implement and advertise 'terminal: true' to enable 'terminal/*'.")
+ f.Type().Id("ClientTerminal").Interface(clientTerminal...)
+ }
+ // Always emit ClientExperimental, even if empty.
+ f.Comment("ClientExperimental defines unstable methods that are not part of the official spec. These may change or be removed without notice.")
+ f.Type().Id("ClientExperimental").Interface(clientExperimental...)
+
+ var buf bytes.Buffer
+ if err := f.Render(&buf); err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(outDir, "types_gen.go"), buf.Bytes(), 0o644)
+}
+
+func isStringConstUnion(def *load.Definition) bool {
+ if def == nil || len(def.OneOf) == 0 {
+ return false
+ }
+ for _, v := range def.OneOf {
+ if v == nil || v.Const == nil {
+ return false
+ }
+ if _, ok := v.Const.(string); !ok {
+ return false
+ }
+ }
+ return true
+}
+
+// emitValidateJen generates validators for selected types (logic unchanged).
+
+func emitValidateJen(f *File, name string, def *load.Definition) {
+ switch name {
+ case "ToolCallUpdate":
+ f.Func().Params(Id("t").Op("*").Id("ToolCallUpdate")).Id("Validate").Params().Params(Error()).Block(
+ If(Id("t").Dot("ToolCallId").Op("==").Lit("")).Block(Return(Qual("fmt", "Errorf").Call(Lit("toolCallId is required")))),
+ Return(Nil()),
+ )
+ return
+ }
+ if def != nil && ir.PrimaryType(def) == "object" {
+ if !strings.HasSuffix(name, "Request") && !strings.HasSuffix(name, "Response") && !strings.HasSuffix(name, "Notification") {
+ return
+ }
+ f.Func().Params(Id("v").Op("*").Id(name)).Id("Validate").Params().Params(Error()).BlockFunc(func(g *Group) {
+ pkeys := make([]string, 0, len(def.Properties))
+ for pk := range def.Properties {
+ pkeys = append(pkeys, pk)
+ }
+ sort.Strings(pkeys)
+ for _, propName := range pkeys {
+ pDef := def.Properties[propName]
+ required := slices.Contains(def.Required, propName)
+ field := util.ToExportedField(propName)
+ if required {
+ switch ir.PrimaryType(pDef) {
+ case "string":
+ g.If(Id("v").Dot(field).Op("==").Lit("")).Block(Return(Qual("fmt", "Errorf").Call(Lit(propName + " is required"))))
+ case "array":
+ g.If(Id("v").Dot(field).Op("==").Nil()).Block(Return(Qual("fmt", "Errorf").Call(Lit(propName + " is required"))))
+ }
+ }
+ }
+ g.Return(Nil())
+ })
+ }
+}
+
+// Type mapping helpers (unchanged behavior vs original)
+func primitiveJenType(t string) Code {
+ switch t {
+ case "string":
+ return String()
+ case "integer":
+ return Int()
+ case "number":
+ return Float64()
+ case "boolean":
+ return Bool()
+ default:
+ return Any()
+ }
+}
+
+// defaultKindOf classifies the JSON Schema default value into a coarse kind.
+func defaultKindOf(val any) int {
+ switch val.(type) {
+ case nil:
+ return 0 // KindNone
+ case []any:
+ return 2 // KindArray
+ case map[string]any:
+ return 3 // KindObject
+ case string, float64, bool:
+ return 1 // KindScalar
+ default:
+ // Fallback: classify by fmt string
+ s := fmt.Sprint(val)
+ if strings.HasPrefix(s, "[") {
+ return 2
+ }
+ if strings.HasPrefix(s, "map[") || strings.HasPrefix(s, "{") {
+ return 3
+ }
+ return 1
+ }
+}
+
+// includesNull reports whether the property's type union contains null.
+func includesNull(d *load.Definition) bool {
+ if d == nil || d.Type == nil {
+ return false
+ }
+ if arr, ok := d.Type.([]any); ok {
+ for _, v := range arr {
+ if s, ok2 := v.(string); ok2 && s == "null" {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func jenTypeFor(d *load.Definition) Code {
+ if d == nil {
+ return Any()
+ }
+ if d.Ref != "" {
+ if strings.HasPrefix(d.Ref, "#/$defs/") {
+ return Id(d.Ref[len("#/$defs/"):])
+ }
+ return Any()
+ }
+ if len(d.Enum) > 0 {
+ return String()
+ }
+ switch ir.PrimaryType(d) {
+ case "string":
+ return String()
+ case "integer":
+ return Int()
+ case "number":
+ return Float64()
+ case "boolean":
+ return Bool()
+ case "array":
+ return Index().Add(jenTypeFor(d.Items))
+ case "object":
+ if len(d.Properties) == 0 {
+ return Map(String()).Any()
+ }
+ return Map(String()).Any()
+ default:
+ if len(d.AnyOf) > 0 || len(d.OneOf) > 0 {
+ return Any()
+ }
+ return Any()
+ }
+}
+
+// jenTypeForOptional maps unions that include null to pointer types where applicable.
+func jenTypeForOptional(d *load.Definition) Code {
+ if d == nil {
+ return Any()
+ }
+ list := d.AnyOf
+ if len(list) == 0 {
+ list = d.OneOf
+ }
+ // Case: property type is a union like ["string","null"]
+ if arr, ok := d.Type.([]any); ok && len(arr) == 2 {
+ var other string
+ for _, v := range arr {
+ if s, ok2 := v.(string); ok2 {
+ if s == "null" {
+ continue
+ }
+ other = s
+ }
+ }
+ switch other {
+ case "string":
+ return Op("*").String()
+ case "integer":
+ return Op("*").Int()
+ case "number":
+ return Op("*").Float64()
+ case "boolean":
+ return Op("*").Bool()
+ }
+ }
+ if len(list) == 2 {
+ var nonNull *load.Definition
+ for _, e := range list {
+ if e == nil {
+ continue
+ }
+ if s, ok := e.Type.(string); ok && s == "null" {
+ continue
+ }
+ if e.Const != nil {
+ nn := *e
+ nn.Type = "string"
+ nonNull = &nn
+ } else {
+ nonNull = e
+ }
+ }
+ if nonNull != nil {
+ if nonNull.Ref != "" && strings.HasPrefix(nonNull.Ref, "#/$defs/") {
+ return Op("*").Id(nonNull.Ref[len("#/$defs/"):])
+ }
+ switch ir.PrimaryType(nonNull) {
+ case "string":
+ return Op("*").String()
+ case "integer":
+ return Op("*").Int()
+ case "number":
+ return Op("*").Float64()
+ case "boolean":
+ return Op("*").Bool()
+ }
+ }
+ }
+ return jenTypeFor(d)
+}
+
+// emitAvailableCommandInputJen generates a concrete variant type for anyOf and a thin union wrapper
+// that supports JSON unmarshal by probing object shape. Currently the schema defines one variant
+// (title: UnstructuredCommandInput) with a required 'hint' field.
+func emitUnion(f *File, name string, defs []*load.Definition, exactlyOne bool) {
+ type variantInfo struct {
+ fieldName string
+ typeName string
+ required []string
+ isObject bool
+ discValue string
+ constPairs [][2]string
+ isNull bool
+ }
+ variants := []variantInfo{}
+ discKey := ""
+ // discover discriminator key if present (any const property)
+ for _, v := range defs {
+ if v == nil {
+ continue
+ }
+ for k, pd := range v.Properties {
+ if pd != nil && pd.Const != nil {
+ discKey = k
+ break
+ }
+ }
+ if discKey != "" {
+ break
+ }
+ }
+ for idx, v := range defs {
+ if v == nil {
+ continue
+ }
+ // Detect null-only variant
+ isNull := false
+ if s, ok := v.Type.(string); ok && s == "null" {
+ isNull = true
+ }
+ // Determine type name: prefer $ref target name when present; do not treat Title as a rename for $ref.
+ tname := ""
+ if v.Ref != "" && strings.HasPrefix(v.Ref, "#/$defs/") {
+ tname = v.Ref[len("#/$defs/"):]
+ } else if v.Title != "" {
+ tname = v.Title
+ } else {
+ if discKey != "" {
+ if pd := v.Properties[discKey]; pd != nil && pd.Const != nil {
+ s := fmt.Sprint(pd.Const)
+ tname = name + util.ToExportedField(s)
+ }
+ }
+ if tname == "" {
+ tname = name + fmt.Sprintf("Variant%d", idx+1)
+ }
+ }
+ // Ensure Title-derived names are exported (e.g., "stdio" -> "Stdio").
+ tname = util.ToExportedField(tname)
+ fieldName := util.ToExportedField(tname)
+ dv := ""
+ if discKey != "" {
+ if pd := v.Properties[discKey]; pd != nil && pd.Const != nil {
+ s := fmt.Sprint(pd.Const)
+ fieldName = util.ToExportedField(s)
+ dv = s
+ }
+ }
+ isObj := len(v.Properties) > 0
+ // Skip phantom variants that have neither $ref nor object shape nor null (e.g., placeholders with only a title)
+ if !isObj && v.Ref == "" && !isNull {
+ continue
+ }
+ // collect const properties (e.g., type, outcome)
+ consts := [][2]string{}
+ for pk, pd := range v.Properties {
+ if pd != nil && pd.Const != nil {
+ if s, ok := pd.Const.(string); ok {
+ consts = append(consts, [2]string{pk, s})
+ }
+ }
+ }
+ if (isObj || isNull) && v.Ref == "" {
+ st := []Code{}
+ if !isNull {
+ req := map[string]struct{}{}
+ for _, r := range v.Required {
+ req[r] = struct{}{}
+ }
+ pkeys := make([]string, 0, len(v.Properties))
+ for pk := range v.Properties {
+ pkeys = append(pkeys, pk)
+ }
+ sort.Strings(pkeys)
+ if v.Description != "" {
+ f.Comment(util.SanitizeComment(v.Description))
+ }
+ for _, pk := range pkeys {
+ pDef := v.Properties[pk]
+ field := util.ToExportedField(pk)
+ if pDef.Description != "" {
+ st = append(st, Comment(util.SanitizeComment(pDef.Description)))
+ }
+ tag := pk
+ if _, ok := req[pk]; !ok {
+ tag = pk + ",omitempty"
+ }
+ st = append(st, Id(field).Add(jenTypeForOptional(pDef)).Tag(map[string]string{"json": tag}))
+ }
+ }
+ f.Type().Id(tname).Struct(st...)
+ f.Line()
+ }
+ variants = append(variants, variantInfo{fieldName: fieldName, typeName: tname, required: v.Required, isObject: isObj, discValue: dv, constPairs: consts, isNull: isNull})
+ }
+ // wrapper
+ st := []Code{}
+ for _, vi := range variants {
+ st = append(st, Id(vi.fieldName).Op("*").Id(vi.typeName).Tag(map[string]string{"json": "-"}))
+ }
+ f.Type().Id(name).Struct(st...)
+ f.Line()
+ // Unmarshal
+ f.Func().Params(Id("u").Op("*").Id(name)).Id("UnmarshalJSON").Params(Id("b").Index().Byte()).Error().BlockFunc(func(g *Group) {
+ // Handle literal null if a null-only variant exists
+ {
+ varNullHandled := false
+ for _, vi := range variants {
+ if vi.isNull {
+ // emit once for the first null variant
+ if !varNullHandled {
+ g.If(Id("string").Call(Id("b")).Op("==").Lit("null")).Block(
+ Var().Id("v").Id(vi.typeName),
+ Id("u").Dot(vi.fieldName).Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ )
+ varNullHandled = true
+ }
+ }
+ }
+ }
+ g.Var().Id("m").Map(String()).Qual("encoding/json", "RawMessage")
+ g.If(List(Id("err")).Op(":=").Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("m")), Id("err").Op("!=").Nil()).Block(Return(Id("err")))
+ // Prefer discriminator-based dispatch when available (e.g. "type", "outcome")
+ if discKey != "" {
+ g.BlockFunc(func(h *Group) {
+ h.Var().Id("disc").String()
+ h.If(List(Id("v"), Id("ok")).Op(":=").Id("m").Index(Lit(discKey)), Id("ok")).Block(
+ Qual("encoding/json", "Unmarshal").Call(Id("v"), Op("&").Id("disc")),
+ )
+ h.Switch(Id("disc")).BlockFunc(func(sw *Group) {
+ for _, vi := range variants {
+ if vi.discValue != "" {
+ sw.Case(Lit(vi.discValue)).Block(
+ Var().Id("v").Id(vi.typeName),
+ If(Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("v")).Op("!=").Nil()).Block(Return(Qual("errors", "New").Call(Lit("invalid variant payload")))),
+ Id("u").Dot(vi.fieldName).Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ )
+ }
+ }
+ })
+ })
+ }
+ // Special-case: EmbeddedResourceResource variants distinguished by keys
+ if name == "EmbeddedResourceResource" {
+ g.If(List(Id("_"), Id("ok")).Op(":=").Id("m").Index(Lit("text")), Id("ok")).Block(
+ Var().Id("v").Id("TextResourceContents"),
+ If(Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("v")).Op("!=").Nil()).Block(Return(Qual("errors", "New").Call(Lit("invalid variant payload")))),
+ Id("u").Dot("TextResourceContents").Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ )
+ g.If(List(Id("_"), Id("ok")).Op(":=").Id("m").Index(Lit("blob")), Id("ok")).Block(
+ Var().Id("v").Id("BlobResourceContents"),
+ If(Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("v")).Op("!=").Nil()).Block(Return(Qual("errors", "New").Call(Lit("invalid variant payload")))),
+ Id("u").Dot("BlobResourceContents").Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ )
+ }
+ // required-key match
+ for _, vi := range variants {
+ if vi.isObject && len(vi.required) > 0 {
+ g.BlockFunc(func(h *Group) {
+ h.Var().Id("v").Id(vi.typeName)
+ h.Var().Id("match").Bool().Op("=").Lit(true)
+ for _, rk := range vi.required {
+ h.If(List(Id("_"), Id("ok")).Op(":=").Id("m").Index(Lit(rk)), Op("!").Id("ok")).Block(Id("match").Op("=").Lit(false))
+ }
+ h.If(Id("match")).Block(
+ If(Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("v")).Op("!=").Nil()).Block(Return(Qual("errors", "New").Call(Lit("invalid variant payload")))),
+ Id("u").Dot(vi.fieldName).Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ )
+ })
+ }
+ }
+ // fallback: try decode sequentially
+ for _, vi := range variants {
+ g.Block(
+ Var().Id("v").Id(vi.typeName),
+ If(Qual("encoding/json", "Unmarshal").Call(Id("b"), Op("&").Id("v")).Op("==").Nil()).Block(
+ Id("u").Dot(vi.fieldName).Op("=").Op("&").Id("v"),
+ Return(Nil()),
+ ),
+ )
+ }
+ g.Return(Nil())
+ })
+ // Marshal
+ f.Func().Params(Id("u").Id(name)).Id("MarshalJSON").Params().Params(Index().Byte(), Error()).BlockFunc(func(g *Group) {
+ for _, vi := range variants {
+ g.If(Id("u").Dot(vi.fieldName).Op("!=").Nil()).BlockFunc(func(gg *Group) {
+ // Null-only variant encodes to JSON null
+ if vi.isNull {
+ gg.Return(Qual("encoding/json", "Marshal").Call(Nil()))
+ } else {
+ // Marshal variant to map for discriminant injection and shaping
+ gg.Var().Id("m").Map(String()).Any()
+ gg.List(Id("_b"), Id("_e")).Op(":=").Qual("encoding/json", "Marshal").Call(Op("*").Id("u").Dot(vi.fieldName))
+ gg.If(Id("_e").Op("!=").Nil()).Block(Return(Index().Byte().Values(), Id("_e")))
+ gg.If(Qual("encoding/json", "Unmarshal").Call(Id("_b"), Op("&").Id("m")).Op("!=").Nil()).Block(Return(Index().Byte().Values(), Qual("errors", "New").Call(Lit("invalid variant payload"))))
+ // Inject const discriminants
+ if len(vi.constPairs) > 0 {
+ for _, kv := range vi.constPairs {
+ gg.Id("m").Index(Lit(kv[0])).Op("=").Lit(kv[1])
+ }
+ }
+ // Special shaping for ContentBlock variants to preserve exact wire JSON
+ if name == "ContentBlock" {
+ switch vi.discValue {
+ case "text":
+ gg.Block(
+ Var().Id("nm").Map(String()).Any(),
+ Id("nm").Op("=").Make(Map(String()).Any()),
+ Id("nm").Index(Lit("type")).Op("=").Lit("text"),
+ Id("nm").Index(Lit("text")).Op("=").Id("m").Index(Lit("text")),
+ Return(Qual("encoding/json", "Marshal").Call(Id("nm"))),
+ )
+ case "image":
+ gg.Block(
+ Var().Id("nm").Map(String()).Any(),
+ Id("nm").Op("=").Make(Map(String()).Any()),
+ Id("nm").Index(Lit("type")).Op("=").Lit("image"),
+ Id("nm").Index(Lit("data")).Op("=").Id("m").Index(Lit("data")),
+ Id("nm").Index(Lit("mimeType")).Op("=").Id("m").Index(Lit("mimeType")),
+ // Only include uri if present; do not emit null
+ If(List(Id("_v"), Id("_ok")).Op(":=").Id("m").Index(Lit("uri")), Id("_ok")).Block(
+ Id("nm").Index(Lit("uri")).Op("=").Id("_v"),
+ ),
+ Return(Qual("encoding/json", "Marshal").Call(Id("nm"))),
+ )
+ case "audio":
+ gg.Block(
+ Var().Id("nm").Map(String()).Any(),
+ Id("nm").Op("=").Make(Map(String()).Any()),
+ Id("nm").Index(Lit("type")).Op("=").Lit("audio"),
+ Id("nm").Index(Lit("data")).Op("=").Id("m").Index(Lit("data")),
+ Id("nm").Index(Lit("mimeType")).Op("=").Id("m").Index(Lit("mimeType")),
+ Return(Qual("encoding/json", "Marshal").Call(Id("nm"))),
+ )
+ case "resource_link":
+ gg.BlockFunc(func(b *Group) {
+ b.Var().Id("nm").Map(String()).Any()
+ b.Id("nm").Op("=").Make(Map(String()).Any())
+ b.Id("nm").Index(Lit("type")).Op("=").Lit("resource_link")
+ b.Id("nm").Index(Lit("name")).Op("=").Id("m").Index(Lit("name"))
+ b.Id("nm").Index(Lit("uri")).Op("=").Id("m").Index(Lit("uri"))
+ // Only include optional keys if present
+ b.If(List(Id("v1"), Id("ok1")).Op(":=").Id("m").Index(Lit("description")), Id("ok1")).Block(
+ Id("nm").Index(Lit("description")).Op("=").Id("v1"),
+ )
+ b.If(List(Id("v2"), Id("ok2")).Op(":=").Id("m").Index(Lit("mimeType")), Id("ok2")).Block(
+ Id("nm").Index(Lit("mimeType")).Op("=").Id("v2"),
+ )
+ b.If(List(Id("v3"), Id("ok3")).Op(":=").Id("m").Index(Lit("size")), Id("ok3")).Block(
+ Id("nm").Index(Lit("size")).Op("=").Id("v3"),
+ )
+ b.If(List(Id("v4"), Id("ok4")).Op(":=").Id("m").Index(Lit("title")), Id("ok4")).Block(
+ Id("nm").Index(Lit("title")).Op("=").Id("v4"),
+ )
+ b.Return(Qual("encoding/json", "Marshal").Call(Id("nm")))
+ })
+ case "resource":
+ gg.Block(
+ Var().Id("nm").Map(String()).Any(),
+ Id("nm").Op("=").Make(Map(String()).Any()),
+ Id("nm").Index(Lit("type")).Op("=").Lit("resource"),
+ Id("nm").Index(Lit("resource")).Op("=").Id("m").Index(Lit("resource")),
+ Return(Qual("encoding/json", "Marshal").Call(Id("nm"))),
+ )
+ }
+ }
+ // default: remarshal possibly with injected discriminant
+ if name != "ContentBlock" {
+ gg.Return(Qual("encoding/json", "Marshal").Call(Id("m")))
+ }
+ }
+ })
+ }
+ g.Return(Index().Byte().Values(), Nil())
+ })
+ f.Line()
+
+ // Generic validator for oneOf unions: exactly one variant must be set
+ if exactlyOne {
+ f.Func().Params(Id("u").Op("*").Id(name)).Id("Validate").Params().Params(Error()).BlockFunc(func(g *Group) {
+ g.Var().Id("count").Int()
+ for _, vi := range variants {
+ g.If(Id("u").Dot(vi.fieldName).Op("!=").Nil()).Block(Id("count").Op("++"))
+ }
+ g.If(Id("count").Op("!=").Lit(1)).Block(
+ Return(Qual("errors", "New").Call(Lit(name + " must have exactly one variant set"))),
+ )
+ g.Return(Nil())
+ })
+ f.Line()
+ }
+}
diff --git a/go/cmd/generate/internal/ir/ir.go b/go/cmd/generate/internal/ir/ir.go
new file mode 100644
index 00000000..abec4e75
--- /dev/null
+++ b/go/cmd/generate/internal/ir/ir.go
@@ -0,0 +1,271 @@
+// Package ir defines the intermediate representation used by the Go
+// code generator. It organizes methods, bindings, and schema-derived
+// types so the emit package can produce helpers, types, and dispatch code.
+package ir
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/util"
+)
+
+// MethodBinding describes which interface a method belongs to on each side.
+type MethodBinding int
+
+const (
+ BindUnknown MethodBinding = iota
+ // Agent bindings
+ BindAgent
+ BindAgentLoader
+ BindAgentExperimental
+ // Client bindings
+ BindClient
+ BindClientExperimental
+ BindClientTerminal
+)
+
+// MethodInfo captures association between a wire method and its Go types and binding.
+type MethodInfo struct {
+ Side string // "agent" or "client"
+ Method string // wire method, e.g., "session/new"
+ MethodKey string // meta key, e.g., "session_new"
+ Req string // Go type name of Request
+ Resp string // Go type name of Response
+ Notif string // Go type name of Notification
+ Binding MethodBinding
+ DocsIgnored bool
+}
+
+// Groups is a map keyed by side|wire to MethodInfo.
+type Groups map[string]*MethodInfo
+
+func key(side, method string) string { return side + "|" + method }
+
+// PrimaryType mirrors logic from generator: find primary type string from a Definition.
+func PrimaryType(d *load.Definition) string {
+ if d == nil || d.Type == nil {
+ return ""
+ }
+ switch v := d.Type.(type) {
+ case string:
+ return v
+ case []any:
+ var first string
+ for _, e := range v {
+ if s, ok := e.(string); ok {
+ if first == "" {
+ first = s
+ }
+ if s != "null" {
+ return s
+ }
+ }
+ }
+ return first
+ default:
+ return ""
+ }
+}
+
+// IsNullResponse returns true if the response schema is explicitly null or missing.
+func IsNullResponse(def *load.Definition) bool {
+ if def == nil {
+ return true
+ }
+ if s, ok := def.Type.(string); ok && s == "null" {
+ return true
+ }
+ return false
+}
+
+// BuildMethodGroups merges schema-provided links with meta fallback and returns groups.
+func BuildMethodGroups(schema *load.Schema, meta *load.Meta) Groups {
+ groups := Groups{}
+ // From schema
+ for name, def := range schema.Defs {
+ if def == nil || def.XMethod == "" || def.XSide == "" {
+ continue
+ }
+ k := key(def.XSide, def.XMethod)
+ mi := groups[k]
+ if mi == nil {
+ mi = &MethodInfo{Side: def.XSide, Method: def.XMethod}
+ groups[k] = mi
+ }
+ if strings.HasSuffix(name, "Request") {
+ mi.Req = name
+ }
+ if strings.HasSuffix(name, "Response") {
+ mi.Resp = name
+ }
+ if strings.HasSuffix(name, "Notification") {
+ mi.Notif = name
+ }
+ }
+ // From meta fallback (terminal etc.)
+ for mk, wire := range meta.AgentMethods {
+ k := key("agent", wire)
+ if groups[k] == nil {
+ base := inferTypeBaseFromMethodKey(mk)
+ mi := &MethodInfo{Side: "agent", Method: wire}
+ if wire == "session/cancel" {
+ mi.Notif = "CancelNotification"
+ } else {
+ if _, ok := schema.Defs[base+"Request"]; ok {
+ mi.Req = base + "Request"
+ }
+ if _, ok := schema.Defs[base+"Response"]; ok {
+ mi.Resp = base + "Response"
+ }
+ }
+ if mi.Req != "" || mi.Notif != "" {
+ groups[k] = mi
+ }
+ }
+ }
+ for mk, wire := range meta.ClientMethods {
+ k := key("client", wire)
+ if groups[k] == nil {
+ base := inferTypeBaseFromMethodKey(mk)
+ mi := &MethodInfo{Side: "client", Method: wire}
+ if wire == "session/update" {
+ mi.Notif = "SessionNotification"
+ } else {
+ if _, ok := schema.Defs[base+"Request"]; ok {
+ mi.Req = base + "Request"
+ }
+ if _, ok := schema.Defs[base+"Response"]; ok {
+ mi.Resp = base + "Response"
+ }
+ }
+ if mi.Req != "" || mi.Notif != "" {
+ groups[k] = mi
+ }
+ }
+ }
+ // Post-process bindings and docs-ignore
+ for _, mi := range groups {
+ mi.Binding = classifyBinding(schema, mi)
+ mi.DocsIgnored = isDocsIgnoredMethod(schema, mi)
+ }
+ return groups
+}
+
+// classifyBinding determines interface binding for each method.
+func classifyBinding(schema *load.Schema, mi *MethodInfo) MethodBinding {
+ if mi == nil {
+ return BindUnknown
+ }
+ switch mi.Side {
+ case "agent":
+ if mi.Method == "session/load" {
+ return BindAgentLoader
+ }
+ // Route unstable methods to the experimental interface.
+ if isUnstableMethod(schema, mi) {
+ return BindAgentExperimental
+ }
+ return BindAgent
+ case "client":
+ // Route unstable methods to the experimental interface.
+ if isUnstableMethod(schema, mi) {
+ return BindClientExperimental
+ }
+ return BindClient
+ default:
+ return BindUnknown
+ }
+}
+
+// isDocsIgnoredMethod if any associated type (req/resp/notif) marked x-docs-ignore.
+func isDocsIgnoredMethod(schema *load.Schema, mi *MethodInfo) bool {
+ if mi == nil {
+ return false
+ }
+ if mi.Req != "" {
+ if d := schema.Defs[mi.Req]; d != nil && d.DocsIgnore {
+ return true
+ }
+ }
+ if mi.Resp != "" {
+ if d := schema.Defs[mi.Resp]; d != nil && d.DocsIgnore {
+ return true
+ }
+ }
+ if mi.Notif != "" {
+ if d := schema.Defs[mi.Notif]; d != nil && d.DocsIgnore {
+ return true
+ }
+ }
+ return false
+}
+
+// isUnstableMethod returns true if any associated Request/Response/Notification
+// definition is marked as unstable in its description.
+func isUnstableMethod(schema *load.Schema, mi *MethodInfo) bool {
+ if mi == nil {
+ return false
+ }
+ has := func(name string) bool {
+ if name == "" {
+ return false
+ }
+ if d := schema.Defs[name]; d != nil {
+ desc := strings.ToLower(d.Description)
+ if strings.Contains(desc, "unstable") {
+ return true
+ }
+ }
+ return false
+ }
+ return has(mi.Req) || has(mi.Resp) || has(mi.Notif)
+}
+
+// inferTypeBaseFromMethodKey mirrors previous heuristic; prefer schema when available.
+func inferTypeBaseFromMethodKey(methodKey string) string {
+ if methodKey == "terminal_wait_for_exit" {
+ return "WaitForTerminalExit"
+ }
+ parts := strings.Split(methodKey, "_")
+ if len(parts) == 2 {
+ n, v := parts[0], parts[1]
+ switch v {
+ case "new", "create", "release", "wait", "load", "authenticate", "prompt", "cancel", "read", "write":
+ return util.TitleWord(v) + util.TitleWord(n)
+ default:
+ return util.TitleWord(n) + util.TitleWord(v)
+ }
+ }
+ segs := strings.Split(methodKey, "_")
+ for i := range segs {
+ segs[i] = util.TitleWord(segs[i])
+ }
+ return strings.Join(segs, "")
+}
+
+// DispatchMethodNameForNotification deduces trait method name for notifications.
+func DispatchMethodNameForNotification(methodKey, typeName string) string {
+ switch methodKey {
+ case "session_update":
+ return "SessionUpdate"
+ case "session_cancel":
+ return "Cancel"
+ default:
+ if strings.HasSuffix(typeName, "Notification") {
+ return strings.TrimSuffix(typeName, "Notification")
+ }
+ return typeName
+ }
+}
+
+// SortedKeys returns sorted keys of a map.
+func SortedKeys(m map[string]string) []string {
+ ks := make([]string, 0, len(m))
+ for k := range m {
+ ks = append(ks, k)
+ }
+ sort.Strings(ks)
+ return ks
+}
diff --git a/go/cmd/generate/internal/load/load.go b/go/cmd/generate/internal/load/load.go
new file mode 100644
index 00000000..f056f398
--- /dev/null
+++ b/go/cmd/generate/internal/load/load.go
@@ -0,0 +1,96 @@
+// Package load provides utilities to read the ACP JSON schema and
+// accompanying metadata into minimal structures used by the generator.
+package load
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+// Meta mirrors schema/meta.json for method maps and version.
+type Meta struct {
+ Version int `json:"version"`
+ AgentMethods map[string]string `json:"agentMethods"`
+ ClientMethods map[string]string `json:"clientMethods"`
+}
+
+// Schema is a minimal view over schema/schema.json definitions used by the generator.
+type Schema struct {
+ Defs map[string]*Definition `json:"$defs"`
+}
+
+// Definition is a partial JSON Schema node the generator cares about.
+type Definition struct {
+ Description string `json:"description"`
+ Type any `json:"type"`
+ Properties map[string]*Definition `json:"properties"`
+ Required []string `json:"required"`
+ Enum []any `json:"enum"`
+ Items *Definition `json:"items"`
+ Ref string `json:"$ref"`
+ AnyOf []*Definition `json:"anyOf"`
+ OneOf []*Definition `json:"oneOf"`
+ DocsIgnore bool `json:"x-docs-ignore"`
+ Title string `json:"title"`
+ Const any `json:"const"`
+ XSide string `json:"x-side"`
+ XMethod string `json:"x-method"`
+ // Default holds the JSON Schema default value, when present.
+ // Used by generators to synthesize defaulting behavior.
+ Default any `json:"default"`
+
+ // boolSchema records whether this definition was a boolean schema (true/false).
+ // JSON Schema allows boolean schemas, where true matches anything and false matches nothing.
+ // We ignore the semantic difference in codegen and treat both as permissive/unknown shapes.
+ boolSchema *bool `json:"-"`
+}
+
+// UnmarshalJSON allows Definition to decode both object and boolean JSON Schema forms.
+func (d *Definition) UnmarshalJSON(b []byte) error {
+ // Trim whitespace for simple equality checks
+ tb := bytes.TrimSpace(b)
+ if bytes.Equal(tb, []byte("true")) || bytes.Equal(tb, []byte("false")) {
+ v := bytes.Equal(tb, []byte("true"))
+ // Reset to zero-value and record that this was a boolean schema.
+ *d = Definition{}
+ d.boolSchema = &v
+ return nil
+ }
+ // Fallback to normal object decoding
+ type alias Definition
+ var a alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ *d = Definition(a)
+ return nil
+}
+
+// ReadMeta loads schema/meta.json.
+func ReadMeta(schemaDir string) (*Meta, error) {
+ metaBytes, err := os.ReadFile(filepath.Join(schemaDir, "meta.json"))
+ if err != nil {
+ return nil, fmt.Errorf("read meta.json: %w", err)
+ }
+ var meta Meta
+ if err := json.Unmarshal(metaBytes, &meta); err != nil {
+ return nil, fmt.Errorf("parse meta.json: %w", err)
+ }
+ return &meta, nil
+}
+
+// ReadSchema loads schema/schema.json.
+func ReadSchema(schemaDir string) (*Schema, error) {
+ schemaBytes, err := os.ReadFile(filepath.Join(schemaDir, "schema.json"))
+ if err != nil {
+ return nil, fmt.Errorf("read schema.json: %w", err)
+ }
+ var schema Schema
+ if err := json.Unmarshal(schemaBytes, &schema); err != nil {
+ return nil, fmt.Errorf("parse schema.json: %w", err)
+ }
+ return &schema, nil
+}
diff --git a/go/cmd/generate/internal/util/util.go b/go/cmd/generate/internal/util/util.go
new file mode 100644
index 00000000..0ac7871d
--- /dev/null
+++ b/go/cmd/generate/internal/util/util.go
@@ -0,0 +1,78 @@
+// Package util contains small string and identifier helpers used by the
+// code generator for formatting names and comments.
+package util
+
+import (
+ "strings"
+ "unicode"
+)
+
+// SanitizeComment removes backticks and normalizes whitespace for Go comments.
+func SanitizeComment(s string) string {
+ s = strings.ReplaceAll(s, "`", "'")
+ lines := strings.Split(s, "\n")
+ for i := range lines {
+ lines[i] = strings.TrimSpace(lines[i])
+ }
+ return strings.Join(lines, " ")
+}
+
+// TitleWord uppercases the first rune and lowercases the rest.
+func TitleWord(s string) string {
+ if s == "" {
+ return s
+ }
+ r := []rune(s)
+ r[0] = unicode.ToUpper(r[0])
+ for i := 1; i < len(r); i++ {
+ r[i] = unicode.ToLower(r[i])
+ }
+ return string(r)
+}
+
+// SplitCamel splits a camelCase string into tokens.
+func SplitCamel(s string) []string {
+ var parts []string
+ last := 0
+ for i := 1; i < len(s); i++ {
+ if isBoundary(s[i-1], s[i]) {
+ parts = append(parts, s[last:i])
+ last = i
+ }
+ }
+ parts = append(parts, s[last:])
+ return parts
+}
+
+func isBoundary(prev, curr byte) bool {
+ return (prev >= 'a' && prev <= 'z' && curr >= 'A' && curr <= 'Z') || curr == '_'
+}
+
+// ToExportedField converts snake_case or camelCase to PascalCase.
+func ToExportedField(name string) string {
+ parts := strings.Split(name, "_")
+ if len(parts) == 1 {
+ parts = SplitCamel(name)
+ }
+ for i := range parts {
+ parts[i] = TitleWord(parts[i])
+ }
+ return strings.Join(parts, "")
+}
+
+// ToEnumConst builds a const identifier like .
+func ToEnumConst(typeName, val string) string {
+ cleaned := make([]rune, 0, len(val))
+ for _, r := range val {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
+ cleaned = append(cleaned, r)
+ } else {
+ cleaned = append(cleaned, '_')
+ }
+ }
+ parts := strings.FieldsFunc(string(cleaned), func(r rune) bool { return r == '_' })
+ for i := range parts {
+ parts[i] = TitleWord(strings.ToLower(parts[i]))
+ }
+ return typeName + strings.Join(parts, "")
+}
diff --git a/go/cmd/generate/main.go b/go/cmd/generate/main.go
new file mode 100644
index 00000000..7d291187
--- /dev/null
+++ b/go/cmd/generate/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "flag"
+ "os"
+ "path/filepath"
+
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/emit"
+ "github.com/zed-industries/agent-client-protocol/go/cmd/generate/internal/load"
+)
+
+func main() {
+ var schemaDirFlag string
+ var outDirFlag string
+ flag.StringVar(&schemaDirFlag, "schema", "", "path to schema directory (defaults to /schema)")
+ flag.StringVar(&outDirFlag, "out", "", "output directory for generated go files (defaults to /go)")
+ flag.Parse()
+
+ repoRoot := findRepoRoot()
+ schemaDir := schemaDirFlag
+ outDir := outDirFlag
+ if schemaDir == "" {
+ schemaDir = filepath.Join(repoRoot, "schema")
+ }
+ if outDir == "" {
+ outDir = filepath.Join(repoRoot, "go")
+ }
+
+ if err := os.MkdirAll(outDir, 0o755); err != nil {
+ panic(err)
+ }
+
+ meta, err := load.ReadMeta(schemaDir)
+ if err != nil {
+ panic(err)
+ }
+
+ if err := emit.WriteConstantsJen(outDir, meta); err != nil {
+ panic(err)
+ }
+
+ schema, err := load.ReadSchema(schemaDir)
+ if err != nil {
+ panic(err)
+ }
+
+ if err := emit.WriteTypesJen(outDir, schema, meta); err != nil {
+ panic(err)
+ }
+ if err := emit.WriteDispatchJen(outDir, schema, meta); err != nil {
+ panic(err)
+ }
+
+ // Emit helpers after types so they can reference generated structs.
+ if err := emit.WriteHelpersJen(outDir, schema, meta); err != nil {
+ panic(err)
+ }
+}
+
+func findRepoRoot() string {
+ cwd, _ := os.Getwd()
+ dir := cwd
+ for i := 0; i < 10; i++ {
+ if _, err := os.Stat(filepath.Join(dir, "package.json")); err == nil {
+ return dir
+ }
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ break
+ }
+ dir = parent
+ }
+ return cwd
+}
diff --git a/go/connection.go b/go/connection.go
new file mode 100644
index 00000000..2cebcb90
--- /dev/null
+++ b/go/connection.go
@@ -0,0 +1,314 @@
+package acp
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "log/slog"
+ "sync"
+ "sync/atomic"
+)
+
+type anyMessage struct {
+ JSONRPC string `json:"jsonrpc"`
+ ID *json.RawMessage `json:"id,omitempty"`
+ Method string `json:"method,omitempty"`
+ Params json.RawMessage `json:"params,omitempty"`
+ Result json.RawMessage `json:"result,omitempty"`
+ Error *RequestError `json:"error,omitempty"`
+}
+
+type pendingResponse struct {
+ ch chan anyMessage
+}
+
+type MethodHandler func(ctx context.Context, method string, params json.RawMessage) (any, *RequestError)
+
+// Connection is a simple JSON-RPC 2.0 connection over line-delimited JSON.
+type Connection struct {
+ w io.Writer
+ r io.Reader
+ handler MethodHandler
+
+ mu sync.Mutex
+ nextID atomic.Uint64
+ pending map[string]*pendingResponse
+
+ ctx context.Context
+ cancel context.CancelCauseFunc
+
+ logger *slog.Logger
+}
+
+func NewConnection(handler MethodHandler, peerInput io.Writer, peerOutput io.Reader) *Connection {
+ ctx, cancel := context.WithCancelCause(context.Background())
+ c := &Connection{
+ w: peerInput,
+ r: peerOutput,
+ handler: handler,
+ pending: make(map[string]*pendingResponse),
+ ctx: ctx,
+ cancel: cancel,
+ }
+ go c.receive()
+ return c
+}
+
+// SetLogger installs a logger used for internal connection diagnostics.
+// If unset, logs are written via the default logger.
+func (c *Connection) SetLogger(l *slog.Logger) { c.logger = l }
+
+func (c *Connection) loggerOrDefault() *slog.Logger {
+ if c.logger != nil {
+ return c.logger
+ }
+ return slog.Default()
+}
+
+func (c *Connection) receive() {
+ const (
+ initialBufSize = 1024 * 1024
+ maxBufSize = 10 * 1024 * 1024
+ )
+
+ scanner := bufio.NewScanner(c.r)
+ buf := make([]byte, 0, initialBufSize)
+ scanner.Buffer(buf, maxBufSize)
+
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ if len(bytes.TrimSpace(line)) == 0 {
+ continue
+ }
+
+ var msg anyMessage
+ if err := json.Unmarshal(line, &msg); err != nil {
+ c.loggerOrDefault().Error("failed to parse incoming message", "err", err, "raw", string(line))
+ continue
+ }
+
+ switch {
+ case msg.ID != nil && msg.Method == "":
+ c.handleResponse(&msg)
+ case msg.Method != "":
+ go c.handleInbound(&msg)
+ default:
+ c.loggerOrDefault().Error("received message with neither id nor method", "raw", string(line))
+ }
+ }
+
+ c.cancel(errors.New("peer connection closed"))
+ c.loggerOrDefault().Info("peer connection closed")
+}
+
+func (c *Connection) handleResponse(msg *anyMessage) {
+ idStr := string(*msg.ID)
+
+ c.mu.Lock()
+ pr := c.pending[idStr]
+ if pr != nil {
+ delete(c.pending, idStr)
+ }
+ c.mu.Unlock()
+
+ if pr != nil {
+ pr.ch <- *msg
+ }
+}
+
+func (c *Connection) handleInbound(req *anyMessage) {
+ res := anyMessage{JSONRPC: "2.0"}
+ // copy ID if present
+ if req.ID != nil {
+ res.ID = req.ID
+ }
+ if c.handler == nil {
+ if req.ID != nil {
+ res.Error = NewMethodNotFound(req.Method)
+ _ = c.sendMessage(res)
+ }
+ return
+ }
+
+ result, err := c.handler(c.ctx, req.Method, req.Params)
+ if req.ID == nil {
+ // Notification: no response is sent; log handler errors to surface decode failures.
+ if err != nil {
+ c.loggerOrDefault().Error("failed to handle notification", "method", req.Method, "err", err)
+ }
+ return
+ }
+ if err != nil {
+ res.Error = err
+ } else {
+ // marshal result
+ b, mErr := json.Marshal(result)
+ if mErr != nil {
+ res.Error = NewInternalError(map[string]any{"error": mErr.Error()})
+ } else {
+ res.Result = b
+ }
+ }
+ _ = c.sendMessage(res)
+}
+
+func (c *Connection) sendMessage(msg anyMessage) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ msg.JSONRPC = "2.0"
+ b, err := json.Marshal(msg)
+ if err != nil {
+ return err
+ }
+ b = append(b, '\n')
+ _, err = c.w.Write(b)
+ return err
+}
+
+// SendRequest sends a JSON-RPC request and returns a typed result.
+// For methods that do not return a result, use SendRequestNoResult instead.
+func SendRequest[T any](c *Connection, ctx context.Context, method string, params any) (T, error) {
+ var result T
+
+ msg, idKey, err := c.prepareRequest(method, params)
+ if err != nil {
+ return result, err
+ }
+
+ pr := &pendingResponse{ch: make(chan anyMessage, 1)}
+ c.mu.Lock()
+ c.pending[idKey] = pr
+ c.mu.Unlock()
+
+ if err := c.sendMessage(msg); err != nil {
+ c.cleanupPending(idKey)
+ return result, NewInternalError(map[string]any{"error": err.Error()})
+ }
+
+ resp, err := c.waitForResponse(ctx, pr, idKey)
+ if err != nil {
+ return result, err
+ }
+
+ if resp.Error != nil {
+ return result, resp.Error
+ }
+
+ if len(resp.Result) > 0 {
+ if err := json.Unmarshal(resp.Result, &result); err != nil {
+ return result, NewInternalError(map[string]any{"error": err.Error()})
+ }
+ }
+ return result, nil
+}
+
+func (c *Connection) prepareRequest(method string, params any) (anyMessage, string, error) {
+ id := c.nextID.Add(1)
+ idRaw, _ := json.Marshal(id)
+
+ msg := anyMessage{
+ JSONRPC: "2.0",
+ ID: (*json.RawMessage)(&idRaw),
+ Method: method,
+ }
+
+ if params != nil {
+ b, err := json.Marshal(params)
+ if err != nil {
+ return msg, "", NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ msg.Params = b
+ }
+
+ return msg, string(idRaw), nil
+}
+
+func (c *Connection) waitForResponse(ctx context.Context, pr *pendingResponse, idKey string) (anyMessage, error) {
+ select {
+ case resp := <-pr.ch:
+ return resp, nil
+ case <-ctx.Done():
+ c.cleanupPending(idKey)
+ return anyMessage{}, NewInternalError(map[string]any{"error": context.Cause(ctx).Error()})
+ case <-c.Done():
+ return anyMessage{}, NewInternalError(map[string]any{"error": "peer disconnected before response"})
+ }
+}
+
+func (c *Connection) cleanupPending(idKey string) {
+ c.mu.Lock()
+ delete(c.pending, idKey)
+ c.mu.Unlock()
+}
+
+// SendRequestNoResult sends a JSON-RPC request that returns no result payload.
+func (c *Connection) SendRequestNoResult(ctx context.Context, method string, params any) error {
+ msg, idKey, err := c.prepareRequest(method, params)
+ if err != nil {
+ return err
+ }
+
+ pr := &pendingResponse{ch: make(chan anyMessage, 1)}
+ c.mu.Lock()
+ c.pending[idKey] = pr
+ c.mu.Unlock()
+
+ if err := c.sendMessage(msg); err != nil {
+ c.cleanupPending(idKey)
+ return NewInternalError(map[string]any{"error": err.Error()})
+ }
+
+ resp, err := c.waitForResponse(ctx, pr, idKey)
+ if err != nil {
+ return err
+ }
+
+ if resp.Error != nil {
+ return resp.Error
+ }
+ return nil
+}
+
+func (c *Connection) SendNotification(ctx context.Context, method string, params any) error {
+ select {
+ case <-ctx.Done():
+ return NewInternalError(map[string]any{"error": ctx.Err().Error()})
+ default:
+ }
+
+ msg, err := c.prepareNotification(method, params)
+ if err != nil {
+ return err
+ }
+
+ if err := c.sendMessage(msg); err != nil {
+ return NewInternalError(map[string]any{"error": err.Error()})
+ }
+ return nil
+}
+
+func (c *Connection) prepareNotification(method string, params any) (anyMessage, error) {
+ msg := anyMessage{
+ JSONRPC: "2.0",
+ Method: method,
+ }
+
+ if params != nil {
+ b, err := json.Marshal(params)
+ if err != nil {
+ return msg, NewInvalidParams(map[string]any{"error": err.Error()})
+ }
+ msg.Params = b
+ }
+
+ return msg, nil
+}
+
+// Done returns a channel that is closed when the underlying reader loop exits
+// (typically when the peer disconnects or the input stream is closed).
+func (c *Connection) Done() <-chan struct{} {
+ return c.ctx.Done()
+}
diff --git a/go/constants_gen.go b/go/constants_gen.go
new file mode 100644
index 00000000..d966aeea
--- /dev/null
+++ b/go/constants_gen.go
@@ -0,0 +1,31 @@
+// Code generated by acp-go-generator; DO NOT EDIT.
+
+package acp
+
+// ProtocolVersionNumber is the ACP protocol version supported by this SDK.
+const ProtocolVersionNumber = 1
+
+// Agent method names
+const (
+ AgentMethodAuthenticate = "authenticate"
+ AgentMethodInitialize = "initialize"
+ AgentMethodModelSelect = "session/set_model"
+ AgentMethodSessionCancel = "session/cancel"
+ AgentMethodSessionLoad = "session/load"
+ AgentMethodSessionNew = "session/new"
+ AgentMethodSessionPrompt = "session/prompt"
+ AgentMethodSessionSetMode = "session/set_mode"
+)
+
+// Client method names
+const (
+ ClientMethodFsReadTextFile = "fs/read_text_file"
+ ClientMethodFsWriteTextFile = "fs/write_text_file"
+ ClientMethodSessionRequestPermission = "session/request_permission"
+ ClientMethodSessionUpdate = "session/update"
+ ClientMethodTerminalCreate = "terminal/create"
+ ClientMethodTerminalKill = "terminal/kill"
+ ClientMethodTerminalOutput = "terminal/output"
+ ClientMethodTerminalRelease = "terminal/release"
+ ClientMethodTerminalWaitForExit = "terminal/wait_for_exit"
+)
diff --git a/go/defaults_test.go b/go/defaults_test.go
new file mode 100644
index 00000000..f26f752e
--- /dev/null
+++ b/go/defaults_test.go
@@ -0,0 +1,129 @@
+package acp
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+// Ensure InitializeResponse.authMethods encodes to [] when nil or empty,
+// and decodes to [] when missing or null.
+func TestInitializeResponse_AuthMethods_Defaults(t *testing.T) {
+ t.Parallel()
+ t.Run("marshal_nil_slice_encodes_empty_array", func(t *testing.T) {
+ t.Parallel()
+ resp := InitializeResponse{ProtocolVersion: 1}
+ b, err := json.Marshal(resp)
+ if err != nil {
+ t.Fatalf("marshal error: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(b, &m); err != nil {
+ t.Fatalf("roundtrip unmarshal error: %v", err)
+ }
+ v, ok := m["authMethods"]
+ if !ok {
+ t.Fatalf("authMethods missing in JSON: %s", string(b))
+ }
+ arr, ok := v.([]any)
+ if !ok || len(arr) != 0 {
+ t.Fatalf("authMethods should be empty array; got: %#v (json=%s)", v, string(b))
+ }
+ })
+
+ t.Run("marshal_empty_slice_encodes_empty_array", func(t *testing.T) {
+ t.Parallel()
+ resp := InitializeResponse{ProtocolVersion: 1, AuthMethods: []AuthMethod{}}
+ b, err := json.Marshal(resp)
+ if err != nil {
+ t.Fatalf("marshal error: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(b, &m); err != nil {
+ t.Fatalf("roundtrip unmarshal error: %v", err)
+ }
+ v, ok := m["authMethods"]
+ if !ok {
+ t.Fatalf("authMethods missing in JSON: %s", string(b))
+ }
+ arr, ok := v.([]any)
+ if !ok || len(arr) != 0 {
+ t.Fatalf("authMethods should be empty array; got: %#v (json=%s)", v, string(b))
+ }
+ })
+
+ t.Run("unmarshal_missing_sets_empty_array", func(t *testing.T) {
+ t.Parallel()
+ var resp InitializeResponse
+ if err := json.Unmarshal([]byte(`{"protocolVersion":1}`), &resp); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+ if resp.AuthMethods == nil || len(resp.AuthMethods) != 0 {
+ t.Fatalf("expected default empty authMethods; got: %#v", resp.AuthMethods)
+ }
+ })
+
+ t.Run("unmarshal_null_sets_empty_array", func(t *testing.T) {
+ t.Parallel()
+ var resp InitializeResponse
+ if err := json.Unmarshal([]byte(`{"protocolVersion":1, "authMethods": null}`), &resp); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+ if resp.AuthMethods == nil || len(resp.AuthMethods) != 0 {
+ t.Fatalf("expected default empty authMethods on null; got: %#v", resp.AuthMethods)
+ }
+ })
+}
+
+// Ensure InitializeRequest.clientCapabilities defaults apply on decode when missing,
+// and that the property is present on encode even when zero-value.
+func TestInitializeRequest_ClientCapabilities_Defaults(t *testing.T) {
+ t.Parallel()
+ t.Run("unmarshal_missing_applies_defaults", func(t *testing.T) {
+ t.Parallel()
+ var req InitializeRequest
+ if err := json.Unmarshal([]byte(`{"protocolVersion":1}`), &req); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+ // Defaults per schema: terminal=false; fs.readTextFile=false; fs.writeTextFile=false
+ if req.ClientCapabilities.Terminal != false ||
+ req.ClientCapabilities.Fs.ReadTextFile != false ||
+ req.ClientCapabilities.Fs.WriteTextFile != false {
+ t.Fatalf("unexpected clientCapabilities defaults: %+v", req.ClientCapabilities)
+ }
+ })
+
+ t.Run("marshal_zero_includes_property", func(t *testing.T) {
+ t.Parallel()
+ req := InitializeRequest{ProtocolVersion: 1}
+ b, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("marshal error: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(b, &m); err != nil {
+ t.Fatalf("roundtrip unmarshal error: %v", err)
+ }
+ if _, ok := m["clientCapabilities"]; !ok {
+ t.Fatalf("clientCapabilities should be present in JSON: %s", string(b))
+ }
+ })
+}
+
+// Ensure InitializeResponse.agentCapabilities defaults apply on decode when missing.
+func TestInitializeResponse_AgentCapabilities_Defaults(t *testing.T) {
+ t.Parallel()
+ t.Run("unmarshal_missing_applies_defaults", func(t *testing.T) {
+ t.Parallel()
+ var resp InitializeResponse
+ if err := json.Unmarshal([]byte(`{"protocolVersion":1}`), &resp); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+ // Defaults: loadSession=false; promptCapabilities audio=false, embeddedContext=false, image=false
+ if resp.AgentCapabilities.LoadSession != false ||
+ resp.AgentCapabilities.PromptCapabilities.Audio != false ||
+ resp.AgentCapabilities.PromptCapabilities.EmbeddedContext != false ||
+ resp.AgentCapabilities.PromptCapabilities.Image != false {
+ t.Fatalf("unexpected agentCapabilities defaults: %+v", resp.AgentCapabilities)
+ }
+ })
+}
diff --git a/go/doc.go b/go/doc.go
new file mode 100644
index 00000000..f512d829
--- /dev/null
+++ b/go/doc.go
@@ -0,0 +1,5 @@
+// Package acp provides Go types and connection plumbing for the
+// Agent Client Protocol (ACP). It contains generated dispatchers,
+// outbound helpers, shared request/response types, and related
+// utilities used by agents and clients to communicate over ACP.
+package acp
diff --git a/go/errors.go b/go/errors.go
new file mode 100644
index 00000000..c937b913
--- /dev/null
+++ b/go/errors.go
@@ -0,0 +1,73 @@
+package acp
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// RequestError represents a JSON-RPC error response.
+type RequestError struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data any `json:"data,omitempty"`
+}
+
+func (e *RequestError) Error() string {
+ // Prefer a structured, JSON-style string so callers get details by default
+ // similar to the TypeScript client.
+ // Example: {"code":-32603,"message":"Internal error","data":{"details":"..."}}
+ if e == nil {
+ return ""
+ }
+ // Try to pretty-print compact JSON for stability in logs.
+ type view struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data any `json:"data,omitempty"`
+ }
+ v := view{Code: e.Code, Message: e.Message, Data: e.Data}
+ b, err := json.Marshal(v)
+ if err == nil {
+ return string(b)
+ }
+ // Fallback if marshal fails.
+ if e.Data != nil {
+ return fmt.Sprintf("code %d: %s (data: %v)", e.Code, e.Message, e.Data)
+ }
+ return fmt.Sprintf("code %d: %s", e.Code, e.Message)
+}
+
+func NewParseError(data any) *RequestError {
+ return &RequestError{Code: -32700, Message: "Parse error", Data: data}
+}
+
+func NewInvalidRequest(data any) *RequestError {
+ return &RequestError{Code: -32600, Message: "Invalid request", Data: data}
+}
+
+func NewMethodNotFound(method string) *RequestError {
+ return &RequestError{Code: -32601, Message: "Method not found", Data: map[string]any{"method": method}}
+}
+
+func NewInvalidParams(data any) *RequestError {
+ return &RequestError{Code: -32602, Message: "Invalid params", Data: data}
+}
+
+func NewInternalError(data any) *RequestError {
+ return &RequestError{Code: -32603, Message: "Internal error", Data: data}
+}
+
+func NewAuthRequired(data any) *RequestError {
+ return &RequestError{Code: -32000, Message: "Authentication required", Data: data}
+}
+
+// toReqErr coerces arbitrary errors into JSON-RPC RequestError.
+func toReqErr(err error) *RequestError {
+ if err == nil {
+ return nil
+ }
+ if re, ok := err.(*RequestError); ok {
+ return re
+ }
+ return NewInternalError(map[string]any{"error": err.Error()})
+}
diff --git a/go/example/agent/main.go b/go/example/agent/main.go
new file mode 100644
index 00000000..f1dae39c
--- /dev/null
+++ b/go/example/agent/main.go
@@ -0,0 +1,325 @@
+package main
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "os/exec"
+ "os/signal"
+ "sync"
+ "time"
+
+ acp "github.com/zed-industries/agent-client-protocol/go"
+)
+
+type agentSession struct {
+ cancel context.CancelFunc
+}
+
+type exampleAgent struct {
+ conn *acp.AgentSideConnection
+ sessions map[string]*agentSession
+ mu sync.Mutex
+}
+
+var (
+ _ acp.Agent = (*exampleAgent)(nil)
+ _ acp.AgentLoader = (*exampleAgent)(nil)
+ _ acp.AgentExperimental = (*exampleAgent)(nil)
+)
+
+func newExampleAgent() *exampleAgent {
+ return &exampleAgent{sessions: make(map[string]*agentSession)}
+}
+
+// SetSessionMode implements acp.Agent.
+func (a *exampleAgent) SetSessionMode(ctx context.Context, params acp.SetSessionModeRequest) (acp.SetSessionModeResponse, error) {
+ return acp.SetSessionModeResponse{}, nil
+}
+
+// SetSessionModel implements acp.AgentExperimental.
+func (a *exampleAgent) SetSessionModel(ctx context.Context, params acp.SetSessionModelRequest) (acp.SetSessionModelResponse, error) {
+ return acp.SetSessionModelResponse{}, nil
+}
+
+// Implement acp.AgentConnAware to receive the connection after construction.
+func (a *exampleAgent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
+
+func (a *exampleAgent) Initialize(ctx context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
+ return acp.InitializeResponse{
+ ProtocolVersion: acp.ProtocolVersionNumber,
+ AgentCapabilities: acp.AgentCapabilities{
+ LoadSession: false,
+ },
+ }, nil
+}
+
+func (a *exampleAgent) NewSession(ctx context.Context, params acp.NewSessionRequest) (acp.NewSessionResponse, error) {
+ sid := randomID()
+ a.mu.Lock()
+ a.sessions[sid] = &agentSession{}
+ a.mu.Unlock()
+ return acp.NewSessionResponse{SessionId: acp.SessionId(sid)}, nil
+}
+
+func (a *exampleAgent) Authenticate(ctx context.Context, _ acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
+ return acp.AuthenticateResponse{}, nil
+}
+
+func (a *exampleAgent) LoadSession(ctx context.Context, _ acp.LoadSessionRequest) (acp.LoadSessionResponse, error) {
+ return acp.LoadSessionResponse{}, nil
+}
+
+func (a *exampleAgent) Cancel(ctx context.Context, params acp.CancelNotification) error {
+ a.mu.Lock()
+ s, ok := a.sessions[string(params.SessionId)]
+ a.mu.Unlock()
+ if ok && s != nil && s.cancel != nil {
+ s.cancel()
+ }
+ return nil
+}
+
+func (a *exampleAgent) Prompt(_ context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
+ sid := string(params.SessionId)
+ a.mu.Lock()
+ s, ok := a.sessions[sid]
+ a.mu.Unlock()
+ if !ok {
+ return acp.PromptResponse{}, fmt.Errorf("session %s not found", sid)
+ }
+
+ // cancel any previous turn
+ a.mu.Lock()
+ if s.cancel != nil {
+ prev := s.cancel
+ a.mu.Unlock()
+ prev()
+ } else {
+ a.mu.Unlock()
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ a.mu.Lock()
+ s.cancel = cancel
+ a.mu.Unlock()
+
+ // simulate a full turn with streaming updates and a permission request
+ if err := a.simulateTurn(ctx, sid); err != nil {
+ if ctx.Err() != nil {
+ return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
+ }
+ return acp.PromptResponse{}, err
+ }
+ a.mu.Lock()
+ s.cancel = nil
+ a.mu.Unlock()
+ return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
+}
+
+func (a *exampleAgent) simulateTurn(ctx context.Context, sid string) error {
+ // disclaimer: stream a demo notice so clients see it's the example agent
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateAgentMessageText("ACP Go Example Agent — demo only (no AI model)."),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, 250*time.Millisecond); err != nil {
+ return err
+ }
+ // initial message chunk
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateAgentMessageText("I'll help you with that. Let me start by reading some files to understand the current situation."),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+
+ // tool call without permission
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.StartToolCall(
+ acp.ToolCallId("call_1"),
+ "Reading project files",
+ acp.WithStartKind(acp.ToolKindRead),
+ acp.WithStartStatus(acp.ToolCallStatusPending),
+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "/project/README.md"}}),
+ acp.WithStartRawInput(map[string]any{"path": "/project/README.md"}),
+ ),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+
+ // update tool call completed
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateToolCall(
+ acp.ToolCallId("call_1"),
+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
+ acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolContent(acp.TextBlock("# My Project\n\nThis is a sample project..."))}),
+ acp.WithUpdateRawOutput(map[string]any{"content": "# My Project\n\nThis is a sample project..."}),
+ ),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+
+ // more text
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateAgentMessageText(" Now I understand the project structure. I need to make some changes to improve it."),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+
+ // tool call requiring permission
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.StartToolCall(
+ acp.ToolCallId("call_2"),
+ "Modifying critical configuration file",
+ acp.WithStartKind(acp.ToolKindEdit),
+ acp.WithStartStatus(acp.ToolCallStatusPending),
+ acp.WithStartLocations([]acp.ToolCallLocation{{Path: "/project/config.json"}}),
+ acp.WithStartRawInput(map[string]any{"path": "/project/config.json", "content": "{\"database\": {\"host\": \"new-host\"}}"}),
+ ),
+ }); err != nil {
+ return err
+ }
+
+ // request permission for sensitive operation
+ permResp, err := a.conn.RequestPermission(ctx, acp.RequestPermissionRequest{
+ SessionId: acp.SessionId(sid),
+ ToolCall: acp.ToolCallUpdate{
+ ToolCallId: acp.ToolCallId("call_2"),
+ Title: acp.Ptr("Modifying critical configuration file"),
+ Kind: acp.Ptr(acp.ToolKindEdit),
+ Status: acp.Ptr(acp.ToolCallStatusPending),
+ Locations: []acp.ToolCallLocation{{Path: "/home/user/project/config.json"}},
+ RawInput: map[string]any{"path": "/home/user/project/config.json", "content": "{\"database\": {\"host\": \"new-host\"}}"},
+ },
+ Options: []acp.PermissionOption{
+ {Kind: acp.PermissionOptionKindAllowOnce, Name: "Allow this change", OptionId: acp.PermissionOptionId("allow")},
+ {Kind: acp.PermissionOptionKindRejectOnce, Name: "Skip this change", OptionId: acp.PermissionOptionId("reject")},
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ // handle permission outcome
+ if permResp.Outcome.Cancelled != nil {
+ return nil
+ }
+ if permResp.Outcome.Selected == nil {
+ return fmt.Errorf("unexpected permission outcome")
+ }
+ switch string(permResp.Outcome.Selected.OptionId) {
+ case "allow":
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateToolCall(
+ acp.ToolCallId("call_2"),
+ acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
+ acp.WithUpdateRawOutput(map[string]any{"success": true, "message": "Configuration updated"}),
+ acp.WithUpdateTitle("Modifying critical configuration file"),
+ ),
+ }); err != nil {
+ return err
+ }
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateAgentMessageText(" Perfect! I've successfully updated the configuration. The changes have been applied."),
+ }); err != nil {
+ return err
+ }
+ case "reject":
+ if err := pause(ctx, time.Second); err != nil {
+ return err
+ }
+ if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
+ SessionId: acp.SessionId(sid),
+ Update: acp.UpdateAgentMessageText(" I understand you prefer not to make that change. I'll skip the configuration update."),
+ }); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unexpected permission option: %s", permResp.Outcome.Selected.OptionId)
+ }
+ return nil
+}
+
+func randomID() string {
+ var b [12]byte
+ if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
+ // fallback to time-based
+ return fmt.Sprintf("sess_%d", time.Now().UnixNano())
+ }
+ return "sess_" + hex.EncodeToString(b[:])
+}
+
+func pause(ctx context.Context, d time.Duration) error {
+ t := time.NewTimer(d)
+ defer t.Stop()
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-t.C:
+ return nil
+ }
+}
+
+func main() {
+ // If args provided, treat them as client program + args to spawn and connect via stdio.
+ // Otherwise, default to stdio (allowing manual wiring or use by another process).
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
+ defer cancel()
+
+ var (
+ out io.Writer = os.Stdout
+ in io.Reader = os.Stdin
+ cmd *exec.Cmd
+ )
+ if len(os.Args) > 1 {
+ cmd = exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
+ cmd.Stderr = os.Stderr
+ stdin, _ := cmd.StdinPipe()
+ stdout, _ := cmd.StdoutPipe()
+ if err := cmd.Start(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to start client: %v\n", err)
+ os.Exit(1)
+ }
+ out = stdin
+ in = stdout
+ }
+
+ ag := newExampleAgent()
+ asc := acp.NewAgentSideConnection(ag, out, in)
+ asc.SetLogger(slog.Default())
+ ag.SetAgentConnection(asc)
+
+ // Block until the peer disconnects.
+ <-asc.Done()
+
+ if cmd != nil && cmd.Process != nil {
+ _ = cmd.Process.Kill()
+ }
+}
diff --git a/go/example/claude-code/main.go b/go/example/claude-code/main.go
new file mode 100644
index 00000000..9d7f3412
--- /dev/null
+++ b/go/example/claude-code/main.go
@@ -0,0 +1,276 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ acp "github.com/zed-industries/agent-client-protocol/go"
+)
+
+// ClaudeCodeREPL demonstrates connecting to the Claude Code CLI running in ACP mode
+// and providing a simple REPL to send prompts and print streamed updates.
+
+type replClient struct {
+ autoApprove bool
+}
+
+var _ acp.Client = (*replClient)(nil)
+
+func (c *replClient) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
+ if c.autoApprove {
+ // Prefer an allow option if present; otherwise choose the first option.
+ for _, o := range params.Options {
+ if o.Kind == acp.PermissionOptionKindAllowOnce || o.Kind == acp.PermissionOptionKindAllowAlways {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: o.OptionId}}}, nil
+ }
+ }
+ if len(params.Options) > 0 {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: params.Options[0].OptionId}}}, nil
+ }
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Cancelled: &acp.RequestPermissionOutcomeCancelled{}}}, nil
+ }
+
+ title := ""
+ if params.ToolCall.Title != nil {
+ title = *params.ToolCall.Title
+ }
+ fmt.Printf("\n🔐 Permission requested: %s\n", title)
+ fmt.Println("\nOptions:")
+ for i, opt := range params.Options {
+ fmt.Printf(" %d. %s (%s)\n", i+1, opt.Name, opt.Kind)
+ }
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ fmt.Printf("\nChoose an option: ")
+ line, _ := reader.ReadString('\n')
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ idx := -1
+ _, _ = fmt.Sscanf(line, "%d", &idx)
+ idx = idx - 1
+ if idx >= 0 && idx < len(params.Options) {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: params.Options[idx].OptionId}}}, nil
+ }
+ fmt.Println("Invalid option. Please try again.")
+ }
+}
+
+func (c *replClient) SessionUpdate(ctx context.Context, params acp.SessionNotification) error {
+ u := params.Update
+ switch {
+ case u.AgentMessageChunk != nil:
+ content := u.AgentMessageChunk.Content
+ if content.Text != nil {
+ fmt.Printf("[agent] \n%s\n", content.Text.Text)
+ }
+ case u.ToolCall != nil:
+ fmt.Printf("\n🔧 %s (%s)\n", u.ToolCall.Title, u.ToolCall.Status)
+ case u.ToolCallUpdate != nil:
+ fmt.Printf("\n🔧 Tool call `%s` updated: %v\n\n", u.ToolCallUpdate.ToolCallId, u.ToolCallUpdate.Status)
+ case u.Plan != nil:
+ fmt.Println("[plan update]")
+ case u.AgentThoughtChunk != nil:
+ thought := u.AgentThoughtChunk.Content
+ if thought.Text != nil {
+ fmt.Printf("[agent_thought_chunk] \n%s\n", thought.Text.Text)
+ }
+ case u.UserMessageChunk != nil:
+ fmt.Println("[user_message_chunk]")
+ }
+ return nil
+}
+
+func (c *replClient) WriteTextFile(ctx context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ dir := filepath.Dir(params.Path)
+ if dir != "" {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err)
+ }
+ }
+ if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err)
+ }
+ fmt.Printf("[Client] Wrote %d bytes to %s\n", len(params.Content), params.Path)
+ return acp.WriteTextFileResponse{}, nil
+}
+
+func (c *replClient) ReadTextFile(ctx context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ b, err := os.ReadFile(params.Path)
+ if err != nil {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err)
+ }
+ content := string(b)
+ if params.Line != nil || params.Limit != nil {
+ lines := strings.Split(content, "\n")
+ start := 0
+ if params.Line != nil && *params.Line > 0 {
+ start = min(max(*params.Line-1, 0), len(lines))
+ }
+ end := len(lines)
+ if params.Limit != nil && *params.Limit > 0 {
+ if start+*params.Limit < end {
+ end = start + *params.Limit
+ }
+ }
+ content = strings.Join(lines[start:end], "\n")
+ }
+ fmt.Printf("[Client] ReadTextFile: %s (%d bytes)\n", params.Path, len(content))
+ return acp.ReadTextFileResponse{Content: content}, nil
+}
+
+// Optional/UNSTABLE terminal methods: implement as no-ops for example
+func (c *replClient) CreateTerminal(ctx context.Context, params acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) {
+ fmt.Printf("[Client] CreateTerminal: %v\n", params)
+ return acp.CreateTerminalResponse{TerminalId: "term-1"}, nil
+}
+
+func (c *replClient) TerminalOutput(ctx context.Context, params acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) {
+ fmt.Printf("[Client] TerminalOutput: %v\n", params)
+ return acp.TerminalOutputResponse{Output: "", Truncated: false}, nil
+}
+
+func (c *replClient) ReleaseTerminal(ctx context.Context, params acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) {
+ fmt.Printf("[Client] ReleaseTerminal: %v\n", params)
+ return acp.ReleaseTerminalResponse{}, nil
+}
+
+func (c *replClient) WaitForTerminalExit(ctx context.Context, params acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) {
+ fmt.Printf("[Client] WaitForTerminalExit: %v\n", params)
+ return acp.WaitForTerminalExitResponse{}, nil
+}
+
+// KillTerminalCommand implements acp.Client.
+func (c *replClient) KillTerminalCommand(ctx context.Context, params acp.KillTerminalCommandRequest) (acp.KillTerminalCommandResponse, error) {
+ fmt.Printf("[Client] KillTerminalCommand: %v\n", params)
+ return acp.KillTerminalCommandResponse{}, nil
+}
+
+func main() {
+ yolo := flag.Bool("yolo", false, "Auto-approve permission prompts")
+ flag.Parse()
+
+ // Invoke Claude Code via npx
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ cmd := exec.CommandContext(ctx, "npx", "-y", "@zed-industries/claude-code-acp@latest")
+ cmd.Stderr = os.Stderr
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "stdin pipe error: %v\n", err)
+ os.Exit(1)
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "stdout pipe error: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := cmd.Start(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to start Claude Code: %v\n", err)
+ os.Exit(1)
+ }
+
+ client := &replClient{autoApprove: *yolo}
+ conn := acp.NewClientSideConnection(client, stdin, stdout)
+ conn.SetLogger(slog.Default())
+
+ // Initialize
+ initResp, err := conn.Initialize(ctx, acp.InitializeRequest{
+ ProtocolVersion: acp.ProtocolVersionNumber,
+ ClientCapabilities: acp.ClientCapabilities{Fs: acp.FileSystemCapability{ReadTextFile: true, WriteTextFile: true}},
+ })
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("✅ Connected to Claude Code (protocol v%v)\n", initResp.ProtocolVersion)
+
+ // New session
+ newSess, err := conn.NewSession(ctx, acp.NewSessionRequest{Cwd: mustCwd(), McpServers: []acp.McpServer{}})
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("📝 Created session: %s\n", newSess.SessionId)
+
+ fmt.Println("Type a message and press Enter to send. Commands: :cancel, :exit")
+ scanner := bufio.NewScanner(os.Stdin)
+ for {
+ fmt.Print("> ")
+ if !scanner.Scan() {
+ break
+ }
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+ switch line {
+ case ":exit", ":quit":
+ cancel()
+ return
+ case ":cancel":
+ _ = conn.Cancel(ctx, acp.CancelNotification{SessionId: newSess.SessionId})
+ continue
+ }
+ // Send prompt and wait for completion while streaming updates are printed via SessionUpdate
+ if _, err := conn.Prompt(ctx, acp.PromptRequest{
+ SessionId: newSess.SessionId,
+ Prompt: []acp.ContentBlock{acp.TextBlock(line)},
+ }); err != nil {
+ // If it's a JSON-RPC RequestError, surface more detail for troubleshooting
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error: %v\n", err)
+ }
+ }
+ }
+
+ _ = cmd.Process.Kill()
+}
+
+func mustCwd() string {
+ wd, err := os.Getwd()
+ if err != nil {
+ return "."
+ }
+ return wd
+}
diff --git a/go/example/client/main.go b/go/example/client/main.go
new file mode 100644
index 00000000..7c6b9801
--- /dev/null
+++ b/go/example/client/main.go
@@ -0,0 +1,269 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ acp "github.com/zed-industries/agent-client-protocol/go"
+)
+
+type exampleClient struct{}
+
+var _ acp.Client = (*exampleClient)(nil)
+
+func (e *exampleClient) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
+ title := ""
+ if params.ToolCall.Title != nil {
+ title = *params.ToolCall.Title
+ }
+ fmt.Printf("\n🔐 Permission requested: %s\n", title)
+ fmt.Println("\nOptions:")
+ for i, opt := range params.Options {
+ fmt.Printf(" %d. %s (%s)\n", i+1, opt.Name, opt.Kind)
+ }
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ fmt.Printf("\nChoose an option: ")
+ line, _ := reader.ReadString('\n')
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ idx := -1
+ _, _ = fmt.Sscanf(line, "%d", &idx)
+ idx = idx - 1
+ if idx >= 0 && idx < len(params.Options) {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: params.Options[idx].OptionId}}}, nil
+ }
+ fmt.Println("Invalid option. Please try again.")
+ }
+}
+
+func (e *exampleClient) SessionUpdate(ctx context.Context, params acp.SessionNotification) error {
+ u := params.Update
+ switch {
+ case u.AgentMessageChunk != nil:
+ c := u.AgentMessageChunk.Content
+ if c.Text != nil {
+ fmt.Println(c.Text.Text)
+ }
+ case u.ToolCall != nil:
+ fmt.Printf("\n🔧 %s (%s)\n", u.ToolCall.Title, u.ToolCall.Status)
+ case u.ToolCallUpdate != nil:
+ fmt.Printf("\n🔧 Tool call `%s` updated: %v\n\n", u.ToolCallUpdate.ToolCallId, u.ToolCallUpdate.Status)
+ case u.Plan != nil || u.AgentThoughtChunk != nil || u.UserMessageChunk != nil:
+ // Keep output compact for other updates
+ fmt.Println("[", displayUpdateKind(u), "]")
+ }
+ return nil
+}
+
+func displayUpdateKind(u acp.SessionUpdate) string {
+ switch {
+ case u.UserMessageChunk != nil:
+ return "user_message_chunk"
+ case u.AgentMessageChunk != nil:
+ return "agent_message_chunk"
+ case u.AgentThoughtChunk != nil:
+ return "agent_thought_chunk"
+ case u.ToolCall != nil:
+ return "tool_call"
+ case u.ToolCallUpdate != nil:
+ return "tool_call_update"
+ case u.Plan != nil:
+ return "plan"
+ default:
+ return "unknown"
+ }
+}
+
+func (e *exampleClient) WriteTextFile(ctx context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ dir := filepath.Dir(params.Path)
+ if dir != "" {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err)
+ }
+ }
+ if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err)
+ }
+ fmt.Printf("[Client] Wrote %d bytes to %s\n", len(params.Content), params.Path)
+ return acp.WriteTextFileResponse{}, nil
+}
+
+func (e *exampleClient) ReadTextFile(ctx context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ b, err := os.ReadFile(params.Path)
+ if err != nil {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err)
+ }
+ content := string(b)
+ // Apply optional line/limit (1-based line index)
+ if params.Line != nil || params.Limit != nil {
+ lines := strings.Split(content, "\n")
+ start := 0
+ if params.Line != nil && *params.Line > 0 {
+ start = min(max(*params.Line-1, 0), len(lines))
+ }
+ end := len(lines)
+ if params.Limit != nil && *params.Limit > 0 {
+ if start+*params.Limit < end {
+ end = start + *params.Limit
+ }
+ }
+ content = strings.Join(lines[start:end], "\n")
+ }
+ fmt.Printf("[Client] ReadTextFile: %s (%d bytes)\n", params.Path, len(content))
+ return acp.ReadTextFileResponse{Content: content}, nil
+}
+
+// Optional/UNSTABLE terminal methods: implement as no-ops for example
+func (e *exampleClient) CreateTerminal(ctx context.Context, params acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) {
+ fmt.Printf("[Client] CreateTerminal: %v\n", params)
+ return acp.CreateTerminalResponse{TerminalId: "term-1"}, nil
+}
+
+func (e *exampleClient) TerminalOutput(ctx context.Context, params acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) {
+ fmt.Printf("[Client] TerminalOutput: %v\n", params)
+ return acp.TerminalOutputResponse{Output: "", Truncated: false}, nil
+}
+
+func (e *exampleClient) ReleaseTerminal(ctx context.Context, params acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) {
+ fmt.Printf("[Client] ReleaseTerminal: %v\n", params)
+ return acp.ReleaseTerminalResponse{}, nil
+}
+
+func (e *exampleClient) WaitForTerminalExit(ctx context.Context, params acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) {
+ fmt.Printf("[Client] WaitForTerminalExit: %v\n", params)
+ return acp.WaitForTerminalExitResponse{}, nil
+}
+
+// KillTerminalCommand implements acp.Client.
+func (c *exampleClient) KillTerminalCommand(ctx context.Context, params acp.KillTerminalCommandRequest) (acp.KillTerminalCommandResponse, error) {
+ fmt.Printf("[Client] KillTerminalCommand: %v\n", params)
+ return acp.KillTerminalCommandResponse{}, nil
+}
+
+func main() {
+ // If args provided, treat them as agent program + args. Otherwise run the Go agent example.
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ var cmd *exec.Cmd
+ if len(os.Args) > 1 {
+ cmd = exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
+ } else {
+ // Default: run the Go example agent. Detect relative to this client's location.
+ _, filename, _, ok := runtime.Caller(0)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "failed to determine current file location\n")
+ os.Exit(1)
+ }
+
+ // Get directory of this client file and find sibling agent directory
+ clientDir := filepath.Dir(filename)
+ agentPath := filepath.Join(clientDir, "..", "agent")
+
+ if _, err := os.Stat(agentPath); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to find agent directory at %s: %v\n", agentPath, err)
+ os.Exit(1)
+ }
+
+ cmd = exec.CommandContext(ctx, "go", "run", agentPath)
+ }
+ cmd.Stderr = os.Stderr
+ // Set up pipes for stdio
+ stdin, _ := cmd.StdinPipe()
+ stdout, _ := cmd.StdoutPipe()
+ if err := cmd.Start(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to start agent: %v\n", err)
+ os.Exit(1)
+ }
+
+ client := &exampleClient{}
+ conn := acp.NewClientSideConnection(client, stdin, stdout)
+ conn.SetLogger(slog.Default())
+
+ // Initialize
+ initResp, err := conn.Initialize(ctx, acp.InitializeRequest{
+ ProtocolVersion: acp.ProtocolVersionNumber,
+ ClientCapabilities: acp.ClientCapabilities{
+ Fs: acp.FileSystemCapability{ReadTextFile: true, WriteTextFile: true},
+ Terminal: true,
+ },
+ })
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("✅ Connected to agent (protocol v%v)\n", initResp.ProtocolVersion)
+
+ // New session
+ newSess, err := conn.NewSession(ctx, acp.NewSessionRequest{Cwd: mustCwd(), McpServers: []acp.McpServer{}})
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("📝 Created session: %s\n", newSess.SessionId)
+ fmt.Printf("💬 User: Hello, agent!\n\n")
+ fmt.Print(" ")
+
+ // Send prompt
+ if _, err := conn.Prompt(ctx, acp.PromptRequest{
+ SessionId: newSess.SessionId,
+ Prompt: []acp.ContentBlock{acp.TextBlock("Hello, agent!")},
+ }); err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error: %v\n", err)
+ }
+ } else {
+ fmt.Printf("\n\n✅ Agent completed\n")
+ }
+
+ _ = cmd.Process.Kill()
+}
+
+func mustCwd() string {
+ wd, err := os.Getwd()
+ if err != nil {
+ return "."
+ }
+ return wd
+}
diff --git a/go/example/gemini/main.go b/go/example/gemini/main.go
new file mode 100644
index 00000000..ff525ffe
--- /dev/null
+++ b/go/example/gemini/main.go
@@ -0,0 +1,297 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ acp "github.com/zed-industries/agent-client-protocol/go"
+)
+
+// GeminiREPL demonstrates connecting to the Gemini CLI running in ACP mode
+// and providing a simple REPL to send prompts and print streamed updates.
+
+type replClient struct {
+ autoApprove bool
+}
+
+var _ acp.Client = (*replClient)(nil)
+
+func (c *replClient) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) {
+ if c.autoApprove {
+ // Prefer an allow option if present; otherwise choose the first option.
+ for _, o := range params.Options {
+ if o.Kind == acp.PermissionOptionKindAllowOnce || o.Kind == acp.PermissionOptionKindAllowAlways {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: o.OptionId}}}, nil
+ }
+ }
+ if len(params.Options) > 0 {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: params.Options[0].OptionId}}}, nil
+ }
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Cancelled: &acp.RequestPermissionOutcomeCancelled{}}}, nil
+ }
+
+ title := ""
+ if params.ToolCall.Title != nil {
+ title = *params.ToolCall.Title
+ }
+ fmt.Printf("\n🔐 Permission requested: %s\n", title)
+ fmt.Println("\nOptions:")
+ for i, opt := range params.Options {
+ fmt.Printf(" %d. %s (%s)\n", i+1, opt.Name, opt.Kind)
+ }
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ fmt.Printf("\nChoose an option: ")
+ line, _ := reader.ReadString('\n')
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ idx := -1
+ _, _ = fmt.Sscanf(line, "%d", &idx)
+ idx = idx - 1
+ if idx >= 0 && idx < len(params.Options) {
+ return acp.RequestPermissionResponse{Outcome: acp.RequestPermissionOutcome{Selected: &acp.RequestPermissionOutcomeSelected{OptionId: params.Options[idx].OptionId}}}, nil
+ }
+ fmt.Println("Invalid option. Please try again.")
+ }
+}
+
+func (c *replClient) SessionUpdate(ctx context.Context, params acp.SessionNotification) error {
+ u := params.Update
+ switch {
+ case u.AgentMessageChunk != nil:
+ content := u.AgentMessageChunk.Content
+ if content.Text != nil {
+ fmt.Printf("%s", content.Text.Text)
+ }
+ case u.ToolCall != nil:
+ fmt.Printf("\n🔧 %s (%s)\n", u.ToolCall.Title, u.ToolCall.Status)
+ case u.ToolCallUpdate != nil:
+ fmt.Printf("\n🔧 Tool call `%s` updated: %v\n\n", u.ToolCallUpdate.ToolCallId, u.ToolCallUpdate.Status)
+ case u.Plan != nil:
+ fmt.Println("[plan update]")
+ case u.AgentThoughtChunk != nil:
+ thought := u.AgentThoughtChunk.Content
+ if thought.Text != nil {
+ fmt.Printf("[agent_thought_chunk] \n%s\n", thought.Text.Text)
+ }
+ case u.UserMessageChunk != nil:
+ fmt.Println("[user_message_chunk]")
+ }
+ return nil
+}
+
+func (c *replClient) WriteTextFile(ctx context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ dir := filepath.Dir(params.Path)
+ if dir != "" {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err)
+ }
+ }
+ if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil {
+ return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err)
+ }
+ fmt.Printf("[Client] Wrote %d bytes to %s\n", len(params.Content), params.Path)
+ return acp.WriteTextFileResponse{}, nil
+}
+
+func (c *replClient) ReadTextFile(ctx context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) {
+ if !filepath.IsAbs(params.Path) {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path)
+ }
+ b, err := os.ReadFile(params.Path)
+ if err != nil {
+ return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err)
+ }
+ content := string(b)
+ if params.Line != nil || params.Limit != nil {
+ lines := strings.Split(content, "\n")
+ start := 0
+ if params.Line != nil && *params.Line > 0 {
+ start = min(max(*params.Line-1, 0), len(lines))
+ }
+ end := len(lines)
+ if params.Limit != nil && *params.Limit > 0 {
+ if start+*params.Limit < end {
+ end = start + *params.Limit
+ }
+ }
+ content = strings.Join(lines[start:end], "\n")
+ }
+ fmt.Printf("[Client] ReadTextFile: %s (%d bytes)\n", params.Path, len(content))
+ return acp.ReadTextFileResponse{Content: content}, nil
+}
+
+// Optional/UNSTABLE terminal methods: implement as no-ops for example
+func (c *replClient) CreateTerminal(ctx context.Context, params acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) {
+ fmt.Printf("[Client] CreateTerminal: %v\n", params)
+ return acp.CreateTerminalResponse{TerminalId: "term-1"}, nil
+}
+
+func (c *replClient) TerminalOutput(ctx context.Context, params acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) {
+ fmt.Printf("[Client] TerminalOutput: %v\n", params)
+ return acp.TerminalOutputResponse{Output: "", Truncated: false}, nil
+}
+
+func (c *replClient) ReleaseTerminal(ctx context.Context, params acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) {
+ fmt.Printf("[Client] ReleaseTerminal: %v\n", params)
+ return acp.ReleaseTerminalResponse{}, nil
+}
+
+func (c *replClient) WaitForTerminalExit(ctx context.Context, params acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) {
+ fmt.Printf("[Client] WaitForTerminalExit: %v\n", params)
+ return acp.WaitForTerminalExitResponse{}, nil
+}
+
+// KillTerminalCommand implements acp.Client.
+func (c *replClient) KillTerminalCommand(ctx context.Context, params acp.KillTerminalCommandRequest) (acp.KillTerminalCommandResponse, error) {
+ fmt.Printf("[Client] KillTerminalCommand: %v\n", params)
+ return acp.KillTerminalCommandResponse{}, nil
+}
+
+func main() {
+ binary := flag.String("gemini", "gemini", "Path to the Gemini CLI binary")
+ model := flag.String("model", "", "Model to pass to Gemini (optional)")
+ sandbox := flag.Bool("sandbox", false, "Run Gemini in sandbox mode")
+ yolo := flag.Bool("yolo", false, "Auto-approve permission prompts")
+ debug := flag.Bool("debug", false, "Pass --debug to Gemini")
+ flag.Parse()
+
+ args := []string{"--experimental-acp"}
+ if *model != "" {
+ args = append(args, "--model", *model)
+ }
+ if *sandbox {
+ args = append(args, "--sandbox")
+ }
+ if *debug {
+ args = append(args, "--debug")
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, *binary, args...)
+ cmd.Stderr = os.Stderr
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "stdin pipe error: %v\n", err)
+ os.Exit(1)
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "stdout pipe error: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := cmd.Start(); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to start Gemini: %v\n", err)
+ os.Exit(1)
+ }
+
+ client := &replClient{autoApprove: *yolo}
+ conn := acp.NewClientSideConnection(client, stdin, stdout)
+ conn.SetLogger(slog.Default())
+
+ // Initialize
+ initResp, err := conn.Initialize(ctx, acp.InitializeRequest{
+ ProtocolVersion: acp.ProtocolVersionNumber,
+ ClientCapabilities: acp.ClientCapabilities{
+ Fs: acp.FileSystemCapability{ReadTextFile: true, WriteTextFile: true},
+ Terminal: true,
+ },
+ })
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "initialize error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("✅ Connected to Gemini (protocol v%v)\n", initResp.ProtocolVersion)
+
+ // New session
+ newSess, err := conn.NewSession(ctx, acp.NewSessionRequest{
+ Cwd: mustCwd(),
+ McpServers: []acp.McpServer{},
+ })
+ if err != nil {
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "newSession error: %v\n", err)
+ }
+ _ = cmd.Process.Kill()
+ os.Exit(1)
+ }
+ fmt.Printf("📝 Created session: %s\n", newSess.SessionId)
+
+ fmt.Println("Type a message and press Enter to send. Commands: :cancel, :exit")
+ scanner := bufio.NewScanner(os.Stdin)
+ for {
+ fmt.Print("\n> ")
+ if !scanner.Scan() {
+ break
+ }
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+ switch line {
+ case ":exit", ":quit":
+ cancel()
+ return
+ case ":cancel":
+ _ = conn.Cancel(ctx, acp.CancelNotification{SessionId: newSess.SessionId})
+ continue
+ }
+ // Send prompt and wait for completion while streaming updates are printed via SessionUpdate
+ if _, err := conn.Prompt(ctx, acp.PromptRequest{
+ SessionId: newSess.SessionId,
+ Prompt: []acp.ContentBlock{acp.TextBlock(line)},
+ }); err != nil {
+ // If it's a JSON-RPC RequestError, surface more detail for troubleshooting
+ if re, ok := err.(*acp.RequestError); ok {
+ if b, mErr := json.MarshalIndent(re, "", " "); mErr == nil {
+ fmt.Fprintf(os.Stderr, "[Client] Error: %s\n", string(b))
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error (%d): %s\n", re.Code, re.Message)
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "prompt error: %v\n", err)
+ }
+ }
+ }
+
+ _ = cmd.Process.Kill()
+}
+
+func mustCwd() string {
+ wd, err := os.Getwd()
+ if err != nil {
+ return "."
+ }
+ return wd
+}
diff --git a/go/example_agent_test.go b/go/example_agent_test.go
new file mode 100644
index 00000000..c9cc94ab
--- /dev/null
+++ b/go/example_agent_test.go
@@ -0,0 +1,101 @@
+package acp
+
+import (
+ "context"
+ "os"
+)
+
+// agentExample mirrors the go/example/agent flow in a compact form.
+// It streams a short message, demonstrates a tool call + permission,
+// then ends the turn.
+type agentExample struct{ conn *AgentSideConnection }
+
+var _ Agent = (*agentExample)(nil)
+
+// SetSessionMode implements Agent.
+func (a *agentExample) SetSessionMode(ctx context.Context, params SetSessionModeRequest) (SetSessionModeResponse, error) {
+ return SetSessionModeResponse{}, nil
+}
+
+func (a *agentExample) SetAgentConnection(c *AgentSideConnection) { a.conn = c }
+
+func (agentExample) Authenticate(ctx context.Context, _ AuthenticateRequest) (AuthenticateResponse, error) {
+ return AuthenticateResponse{}, nil
+}
+
+func (agentExample) Initialize(ctx context.Context, _ InitializeRequest) (InitializeResponse, error) {
+ return InitializeResponse{
+ ProtocolVersion: ProtocolVersionNumber,
+ AgentCapabilities: AgentCapabilities{LoadSession: false},
+ }, nil
+}
+func (agentExample) Cancel(ctx context.Context, _ CancelNotification) error { return nil }
+func (agentExample) NewSession(ctx context.Context, _ NewSessionRequest) (NewSessionResponse, error) {
+ return NewSessionResponse{SessionId: SessionId("sess_demo")}, nil
+}
+
+func (a *agentExample) Prompt(ctx context.Context, p PromptRequest) (PromptResponse, error) {
+ // Stream an initial agent message.
+ _ = a.conn.SessionUpdate(ctx, SessionNotification{
+ SessionId: p.SessionId,
+ Update: UpdateAgentMessageText("I'll help you with that."),
+ })
+
+ // Announce a tool call.
+ _ = a.conn.SessionUpdate(ctx, SessionNotification{
+ SessionId: p.SessionId,
+ Update: StartToolCall(
+ ToolCallId("call_1"),
+ "Modifying configuration",
+ WithStartKind(ToolKindEdit),
+ WithStartStatus(ToolCallStatusPending),
+ WithStartLocations([]ToolCallLocation{{Path: "/project/config.json"}}),
+ WithStartRawInput(map[string]any{"path": "/project/config.json"}),
+ ),
+ })
+
+ // Ask the client for permission to proceed with the change.
+ resp, _ := a.conn.RequestPermission(ctx, RequestPermissionRequest{
+ SessionId: p.SessionId,
+ ToolCall: ToolCallUpdate{
+ ToolCallId: ToolCallId("call_1"),
+ Title: Ptr("Modifying configuration"),
+ Kind: Ptr(ToolKindEdit),
+ Status: Ptr(ToolCallStatusPending),
+ Locations: []ToolCallLocation{{Path: "/project/config.json"}},
+ RawInput: map[string]any{"path": "/project/config.json"},
+ },
+ Options: []PermissionOption{
+ {Kind: PermissionOptionKindAllowOnce, Name: "Allow", OptionId: PermissionOptionId("allow")},
+ {Kind: PermissionOptionKindRejectOnce, Name: "Reject", OptionId: PermissionOptionId("reject")},
+ },
+ })
+
+ if resp.Outcome.Selected != nil && string(resp.Outcome.Selected.OptionId) == "allow" {
+ // Mark tool call completed and stream a final message.
+ _ = a.conn.SessionUpdate(ctx, SessionNotification{
+ SessionId: p.SessionId,
+ Update: UpdateToolCall(
+ ToolCallId("call_1"),
+ WithUpdateStatus(ToolCallStatusCompleted),
+ WithUpdateRawOutput(map[string]any{"success": true}),
+ ),
+ })
+ _ = a.conn.SessionUpdate(ctx, SessionNotification{
+ SessionId: p.SessionId,
+ Update: UpdateAgentMessageText("Done."),
+ })
+ }
+
+ return PromptResponse{StopReason: StopReasonEndTurn}, nil
+}
+
+// Example_agent wires the Agent to stdio so an external client
+// can connect via this process' stdin/stdout.
+func Example_agent() {
+ ag := &agentExample{}
+ asc := NewAgentSideConnection(ag, os.Stdout, os.Stdin)
+ ag.SetAgentConnection(asc)
+ // In a real program, block until the peer disconnects:
+ // <-asc.Done()
+}
diff --git a/go/example_client_test.go b/go/example_client_test.go
new file mode 100644
index 00000000..6810db1d
--- /dev/null
+++ b/go/example_client_test.go
@@ -0,0 +1,143 @@
+package acp
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+// clientExample mirrors go/example/client in a compact form: prints
+// streamed updates, handles simple file ops, and picks the first
+// permission option.
+type clientExample struct{}
+
+var _ Client = (*clientExample)(nil)
+
+func (clientExample) RequestPermission(ctx context.Context, p RequestPermissionRequest) (RequestPermissionResponse, error) {
+ if len(p.Options) == 0 {
+ return RequestPermissionResponse{
+ Outcome: RequestPermissionOutcome{
+ Cancelled: &RequestPermissionOutcomeCancelled{},
+ },
+ }, nil
+ }
+ return RequestPermissionResponse{
+ Outcome: RequestPermissionOutcome{
+ Selected: &RequestPermissionOutcomeSelected{OptionId: p.Options[0].OptionId},
+ },
+ }, nil
+}
+
+func (clientExample) SessionUpdate(ctx context.Context, n SessionNotification) error {
+ u := n.Update
+ switch {
+ case u.AgentMessageChunk != nil:
+ c := u.AgentMessageChunk.Content
+ if c.Text != nil {
+ fmt.Print(c.Text.Text)
+ }
+ case u.ToolCall != nil:
+ title := u.ToolCall.Title
+ fmt.Printf("\n[tool] %s (%s)\n", title, u.ToolCall.Status)
+ case u.ToolCallUpdate != nil:
+ fmt.Printf("\n[tool] %s -> %v\n", u.ToolCallUpdate.ToolCallId, u.ToolCallUpdate.Status)
+ }
+ return nil
+}
+
+func (clientExample) WriteTextFile(ctx context.Context, p WriteTextFileRequest) (WriteTextFileResponse, error) {
+ if !filepath.IsAbs(p.Path) {
+ return WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", p.Path)
+ }
+ if dir := filepath.Dir(p.Path); dir != "" {
+ _ = os.MkdirAll(dir, 0o755)
+ }
+ return WriteTextFileResponse{}, os.WriteFile(p.Path, []byte(p.Content), 0o644)
+}
+
+func (clientExample) ReadTextFile(ctx context.Context, p ReadTextFileRequest) (ReadTextFileResponse, error) {
+ if !filepath.IsAbs(p.Path) {
+ return ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", p.Path)
+ }
+ b, err := os.ReadFile(p.Path)
+ if err != nil {
+ return ReadTextFileResponse{}, err
+ }
+ content := string(b)
+ if p.Line != nil || p.Limit != nil {
+ lines := strings.Split(content, "\n")
+ start := 0
+ if p.Line != nil && *p.Line > 0 {
+ if *p.Line-1 > 0 {
+ start = *p.Line - 1
+ }
+ if start > len(lines) {
+ start = len(lines)
+ }
+ }
+ end := len(lines)
+ if p.Limit != nil && *p.Limit > 0 && start+*p.Limit < end {
+ end = start + *p.Limit
+ }
+ content = strings.Join(lines[start:end], "\n")
+ }
+ return ReadTextFileResponse{Content: content}, nil
+}
+
+// Terminal interface implementations (minimal stubs for examples)
+func (clientExample) CreateTerminal(ctx context.Context, p CreateTerminalRequest) (CreateTerminalResponse, error) {
+ // Return a dummy terminal id
+ return CreateTerminalResponse{TerminalId: "t-1"}, nil
+}
+
+func (clientExample) KillTerminalCommand(ctx context.Context, p KillTerminalCommandRequest) (KillTerminalCommandResponse, error) {
+ return KillTerminalCommandResponse{}, nil
+}
+
+func (clientExample) ReleaseTerminal(ctx context.Context, p ReleaseTerminalRequest) (ReleaseTerminalResponse, error) {
+ return ReleaseTerminalResponse{}, nil
+}
+
+func (clientExample) TerminalOutput(ctx context.Context, p TerminalOutputRequest) (TerminalOutputResponse, error) {
+ // Provide non-empty output to satisfy validation
+ return TerminalOutputResponse{Output: "ok", Truncated: false}, nil
+}
+
+func (clientExample) WaitForTerminalExit(ctx context.Context, p WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error) {
+ return WaitForTerminalExitResponse{}, nil
+}
+
+// Example_client launches the Go agent example, negotiates protocol,
+// opens a session, and sends a simple prompt.
+func Example_client() {
+ ctx := context.Background()
+ cmd := exec.Command("go", "run", "./example/agent")
+ stdin, _ := cmd.StdinPipe()
+ stdout, _ := cmd.StdoutPipe()
+ _ = cmd.Start()
+
+ conn := NewClientSideConnection(clientExample{}, stdin, stdout)
+ _, _ = conn.Initialize(ctx, InitializeRequest{
+ ProtocolVersion: ProtocolVersionNumber,
+ ClientCapabilities: ClientCapabilities{
+ Fs: FileSystemCapability{
+ ReadTextFile: true,
+ WriteTextFile: true,
+ },
+ Terminal: true,
+ },
+ })
+ sess, _ := conn.NewSession(ctx, NewSessionRequest{
+ Cwd: "/",
+ McpServers: []McpServer{},
+ })
+ _, _ = conn.Prompt(ctx, PromptRequest{
+ SessionId: sess.SessionId,
+ Prompt: []ContentBlock{TextBlock("Hello, agent!")},
+ })
+
+ _ = cmd.Process.Kill()
+}
diff --git a/go/example_gemini_test.go b/go/example_gemini_test.go
new file mode 100644
index 00000000..e7f76c8b
--- /dev/null
+++ b/go/example_gemini_test.go
@@ -0,0 +1,89 @@
+package acp
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+)
+
+// geminiClient mirrors go/example/gemini in brief: prints text chunks and
+// selects the first permission option. File ops are no-ops here.
+type geminiClient struct{}
+
+var _ Client = (*geminiClient)(nil)
+
+func (geminiClient) RequestPermission(ctx context.Context, p RequestPermissionRequest) (RequestPermissionResponse, error) {
+ if len(p.Options) == 0 {
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Cancelled: &RequestPermissionOutcomeCancelled{}}}, nil
+ }
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{OptionId: p.Options[0].OptionId}}}, nil
+}
+
+func (geminiClient) SessionUpdate(ctx context.Context, n SessionNotification) error {
+ if n.Update.AgentMessageChunk != nil {
+ c := n.Update.AgentMessageChunk.Content
+ if c.Text != nil {
+ fmt.Print(c.Text.Text)
+ }
+ }
+ return nil
+}
+
+func (geminiClient) ReadTextFile(ctx context.Context, _ ReadTextFileRequest) (ReadTextFileResponse, error) {
+ return ReadTextFileResponse{}, nil
+}
+
+func (geminiClient) WriteTextFile(ctx context.Context, _ WriteTextFileRequest) (WriteTextFileResponse, error) {
+ return WriteTextFileResponse{}, nil
+}
+
+// Terminal interface implementations (minimal stubs for examples)
+func (geminiClient) CreateTerminal(ctx context.Context, p CreateTerminalRequest) (CreateTerminalResponse, error) {
+ return CreateTerminalResponse{TerminalId: "t-1"}, nil
+}
+
+func (geminiClient) KillTerminalCommand(ctx context.Context, p KillTerminalCommandRequest) (KillTerminalCommandResponse, error) {
+ return KillTerminalCommandResponse{}, nil
+}
+
+func (geminiClient) ReleaseTerminal(ctx context.Context, p ReleaseTerminalRequest) (ReleaseTerminalResponse, error) {
+ return ReleaseTerminalResponse{}, nil
+}
+
+func (geminiClient) TerminalOutput(ctx context.Context, p TerminalOutputRequest) (TerminalOutputResponse, error) {
+ return TerminalOutputResponse{Output: "ok", Truncated: false}, nil
+}
+
+func (geminiClient) WaitForTerminalExit(ctx context.Context, p WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error) {
+ return WaitForTerminalExitResponse{}, nil
+}
+
+// Example_gemini connects to a Gemini CLI speaking ACP over stdio,
+// then initializes, opens a session, and sends a prompt.
+func Example_gemini() {
+ ctx := context.Background()
+ cmd := exec.Command("gemini", "--experimental-acp")
+ stdin, _ := cmd.StdinPipe()
+ stdout, _ := cmd.StdoutPipe()
+ _ = cmd.Start()
+
+ conn := NewClientSideConnection(geminiClient{}, stdin, stdout)
+ _, _ = conn.Initialize(ctx, InitializeRequest{
+ ProtocolVersion: ProtocolVersionNumber,
+ ClientCapabilities: ClientCapabilities{
+ Fs: FileSystemCapability{
+ ReadTextFile: true,
+ WriteTextFile: true,
+ },
+ Terminal: true,
+ },
+ })
+ sess, _ := conn.NewSession(ctx, NewSessionRequest{
+ Cwd: "/",
+ McpServers: []McpServer{},
+ })
+ _, _ = conn.Prompt(ctx, PromptRequest{
+ SessionId: sess.SessionId,
+ Prompt: []ContentBlock{TextBlock("list files")},
+ })
+}
diff --git a/go/helpers.go b/go/helpers.go
new file mode 100644
index 00000000..cedfea24
--- /dev/null
+++ b/go/helpers.go
@@ -0,0 +1,259 @@
+package acp
+
+// TextBlock constructs a text content block.
+func TextBlock(text string) ContentBlock {
+ return ContentBlock{Text: &ContentBlockText{
+ Text: text,
+ Type: "text",
+ }}
+}
+
+// ImageBlock constructs an inline image content block with base64-encoded data.
+func ImageBlock(data string, mimeType string) ContentBlock {
+ return ContentBlock{Image: &ContentBlockImage{
+ Data: data,
+ MimeType: mimeType,
+ Type: "image",
+ }}
+}
+
+// AudioBlock constructs an inline audio content block with base64-encoded data.
+func AudioBlock(data string, mimeType string) ContentBlock {
+ return ContentBlock{Audio: &ContentBlockAudio{
+ Data: data,
+ MimeType: mimeType,
+ Type: "audio",
+ }}
+}
+
+// ResourceLinkBlock constructs a resource_link content block with a name and URI.
+func ResourceLinkBlock(name string, uri string) ContentBlock {
+ return ContentBlock{ResourceLink: &ContentBlockResourceLink{
+ Name: name,
+ Type: "resource_link",
+ Uri: uri,
+ }}
+}
+
+// ResourceBlock wraps an embedded resource as a content block.
+func ResourceBlock(res EmbeddedResource) ContentBlock {
+ return ContentBlock{Resource: &ContentBlockResource{
+ Resource: res.Resource,
+ Type: "resource",
+ }}
+}
+
+// ToolContent wraps a content block as tool-call content.
+func ToolContent(block ContentBlock) ToolCallContent {
+ return ToolCallContent{Content: &ToolCallContentContent{
+ Content: block,
+ Type: "content",
+ }}
+}
+
+// ToolDiffContent constructs a diff tool-call content. If oldText is omitted, the field is left empty.
+func ToolDiffContent(path string, newText string, oldText ...string) ToolCallContent {
+ var o *string
+ if len(oldText) > 0 {
+ o = &oldText[0]
+ }
+ return ToolCallContent{Diff: &ToolCallContentDiff{
+ NewText: newText,
+ OldText: o,
+ Path: path,
+ Type: "diff",
+ }}
+}
+
+// ToolTerminalRef constructs a terminal reference tool-call content.
+func ToolTerminalRef(terminalID string) ToolCallContent {
+ return ToolCallContent{Terminal: &ToolCallContentTerminal{
+ TerminalId: terminalID,
+ Type: "terminal",
+ }}
+}
+
+// Ptr returns a pointer to v.
+func Ptr[T any](v T) *T {
+ return &v
+}
+
+// UpdateUserMessage constructs a user_message_chunk update with the given content.
+func UpdateUserMessage(content ContentBlock) SessionUpdate {
+ return SessionUpdate{UserMessageChunk: &SessionUpdateUserMessageChunk{Content: content}}
+}
+
+// UpdateUserMessageText constructs a user_message_chunk update from text.
+func UpdateUserMessageText(text string) SessionUpdate {
+ return UpdateUserMessage(TextBlock(text))
+}
+
+// UpdateAgentMessage constructs an agent_message_chunk update with the given content.
+func UpdateAgentMessage(content ContentBlock) SessionUpdate {
+ return SessionUpdate{AgentMessageChunk: &SessionUpdateAgentMessageChunk{Content: content}}
+}
+
+// UpdateAgentMessageText constructs an agent_message_chunk update from text.
+func UpdateAgentMessageText(text string) SessionUpdate {
+ return UpdateAgentMessage(TextBlock(text))
+}
+
+// UpdateAgentThought constructs an agent_thought_chunk update with the given content.
+func UpdateAgentThought(content ContentBlock) SessionUpdate {
+ return SessionUpdate{AgentThoughtChunk: &SessionUpdateAgentThoughtChunk{Content: content}}
+}
+
+// UpdateAgentThoughtText constructs an agent_thought_chunk update from text.
+func UpdateAgentThoughtText(text string) SessionUpdate {
+ return UpdateAgentThought(TextBlock(text))
+}
+
+// UpdatePlan constructs a plan update with the provided entries.
+func UpdatePlan(entries ...PlanEntry) SessionUpdate {
+ return SessionUpdate{Plan: &SessionUpdatePlan{Entries: entries}}
+}
+
+type ToolCallStartOpt func(tc *SessionUpdateToolCall)
+
+// StartToolCall constructs a tool_call update with required fields and applies optional modifiers.
+func StartToolCall(id ToolCallId, title string, opts ...ToolCallStartOpt) SessionUpdate {
+ tc := SessionUpdateToolCall{
+ Title: title,
+ ToolCallId: id,
+ }
+ for _, opt := range opts {
+ opt(&tc)
+ }
+ return SessionUpdate{ToolCall: &tc}
+}
+
+// WithStartKind sets the kind for a tool_call start update.
+func WithStartKind(k ToolKind) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.Kind = k
+ }
+}
+
+// WithStartStatus sets the status for a tool_call start update.
+func WithStartStatus(s ToolCallStatus) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.Status = s
+ }
+}
+
+// WithStartContent sets the initial content for a tool_call start update.
+func WithStartContent(c []ToolCallContent) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.Content = c
+ }
+}
+
+// WithStartLocations sets file locations and, if a single path is provided and rawInput is empty, mirrors it as rawInput.path.
+func WithStartLocations(l []ToolCallLocation) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.Locations = l
+ if len(l) == 1 && l[0].Path != "" {
+ if tc.RawInput == nil {
+ tc.RawInput = map[string]any{"path": l[0].Path}
+ } else {
+ m, ok := tc.RawInput.(map[string]any)
+ if ok {
+ if _, exists := m["path"]; !exists {
+ m["path"] = l[0].Path
+ }
+ }
+ }
+ }
+ }
+}
+
+// WithStartRawInput sets rawInput for a tool_call start update.
+func WithStartRawInput(v any) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.RawInput = v
+ }
+}
+
+// WithStartRawOutput sets rawOutput for a tool_call start update.
+func WithStartRawOutput(v any) ToolCallStartOpt {
+ return func(tc *SessionUpdateToolCall) {
+ tc.RawOutput = v
+ }
+}
+
+type ToolCallUpdateOpt func(tu *SessionUpdateToolCallUpdate)
+
+// UpdateToolCall constructs a tool_call_update with the given ID and applies optional modifiers.
+func UpdateToolCall(id ToolCallId, opts ...ToolCallUpdateOpt) SessionUpdate {
+ tu := SessionUpdateToolCallUpdate{ToolCallId: id}
+ for _, opt := range opts {
+ opt(&tu)
+ }
+ return SessionUpdate{ToolCallUpdate: &tu}
+}
+
+// WithUpdateTitle sets the title for a tool_call_update.
+func WithUpdateTitle(t string) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.Title = Ptr(t)
+ }
+}
+
+// WithUpdateKind sets the kind for a tool_call_update.
+func WithUpdateKind(k ToolKind) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.Kind = Ptr(k)
+ }
+}
+
+// WithUpdateStatus sets the status for a tool_call_update.
+func WithUpdateStatus(s ToolCallStatus) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.Status = Ptr(s)
+ }
+}
+
+// WithUpdateContent replaces the content collection for a tool_call_update.
+func WithUpdateContent(c []ToolCallContent) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.Content = c
+ }
+}
+
+// WithUpdateLocations replaces the locations collection for a tool_call_update.
+func WithUpdateLocations(l []ToolCallLocation) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.Locations = l
+ }
+}
+
+// WithUpdateRawInput sets rawInput for a tool_call_update.
+func WithUpdateRawInput(v any) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.RawInput = v
+ }
+}
+
+// WithUpdateRawOutput sets rawOutput for a tool_call_update.
+func WithUpdateRawOutput(v any) ToolCallUpdateOpt {
+ return func(tu *SessionUpdateToolCallUpdate) {
+ tu.RawOutput = v
+ }
+}
+
+// StartReadToolCall constructs a 'tool_call' update for reading a file: kind=read, status=pending, locations=[{path}], rawInput={path}.
+func StartReadToolCall(id ToolCallId, title string, path string, opts ...ToolCallStartOpt) SessionUpdate {
+ base := []ToolCallStartOpt{WithStartKind(ToolKindRead), WithStartStatus(ToolCallStatusPending), WithStartLocations([]ToolCallLocation{{Path: path}}), WithStartRawInput(map[string]any{"path": path})}
+ args := append(base, opts...)
+ return StartToolCall(id, title, args...)
+}
+
+// StartEditToolCall constructs a 'tool_call' update for editing content: kind=edit, status=pending, locations=[{path}], rawInput={path, content}.
+func StartEditToolCall(id ToolCallId, title string, path string, content any, opts ...ToolCallStartOpt) SessionUpdate {
+ base := []ToolCallStartOpt{WithStartKind(ToolKindEdit), WithStartStatus(ToolCallStatusPending), WithStartLocations([]ToolCallLocation{{Path: path}}), WithStartRawInput(map[string]any{
+ "content": content,
+ "path": path,
+ })}
+ args := append(base, opts...)
+ return StartToolCall(id, title, args...)
+}
diff --git a/go/helpers_gen.go b/go/helpers_gen.go
new file mode 100644
index 00000000..510e36a8
--- /dev/null
+++ b/go/helpers_gen.go
@@ -0,0 +1,16 @@
+// Code generated by acp-go-generator; DO NOT EDIT.
+
+package acp
+
+// NewRequestPermissionOutcomeCancelled constructs a RequestPermissionOutcome using the 'cancelled' variant.
+func NewRequestPermissionOutcomeCancelled() RequestPermissionOutcome {
+ return RequestPermissionOutcome{Cancelled: &RequestPermissionOutcomeCancelled{Outcome: "cancelled"}}
+}
+
+// NewRequestPermissionOutcomeSelected constructs a RequestPermissionOutcome using the 'selected' variant.
+func NewRequestPermissionOutcomeSelected(optionId PermissionOptionId) RequestPermissionOutcome {
+ return RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{
+ OptionId: optionId,
+ Outcome: "selected",
+ }}
+}
diff --git a/go/json_parity_test.go b/go/json_parity_test.go
new file mode 100644
index 00000000..13d1aa36
--- /dev/null
+++ b/go/json_parity_test.go
@@ -0,0 +1,272 @@
+package acp
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// normalize unmarshals both sides to generic values and compare structurally.
+func equalJSON(a, b []byte) (bool, string, string) {
+ var va any
+ var vb any
+ if err := json.Unmarshal(a, &va); err != nil {
+ return false, string(a), string(b)
+ }
+ if err := json.Unmarshal(b, &vb); err != nil {
+ return false, string(a), string(b)
+ }
+ return reflect.DeepEqual(va, vb), string(a), string(b)
+}
+
+func mustReadGolden(t *testing.T, name string) []byte {
+ t.Helper()
+ p := filepath.Join("testdata", "json_golden", name)
+ b, err := os.ReadFile(p)
+ if err != nil {
+ t.Fatalf("read golden %s: %v", p, err)
+ }
+ return b
+}
+
+// Generic golden runner for a specific type T. Accepts one or more builders and
+// returns a subtest function that asserts they all serialize to the same golden
+// file derived from the subtest name.
+func runGolden[T any](builds ...func() T) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Helper()
+ t.Parallel()
+ // Use the current subtest name; expect pattern like "/".
+ name := t.Name()
+ base := name
+ if i := strings.LastIndex(base, "/"); i >= 0 {
+ base = base[i+1:]
+ }
+ want := mustReadGolden(t, base+".json")
+ // Forward serialization for each builder matches the same golden JSON.
+ for _, build := range builds {
+ got, err := json.Marshal(build())
+ if err != nil {
+ t.Fatalf("marshal %s: %v", base, err)
+ }
+ if ok, ga, gw := equalJSON(got, want); !ok {
+ t.Fatalf("%s marshal mismatch\n got: %s\nwant: %s", base, ga, gw)
+ }
+ }
+ // Unmarshal golden into type, then marshal again and compare (one round-trip check).
+ var v T
+ if err := json.Unmarshal(want, &v); err != nil {
+ t.Fatalf("unmarshal %s: %v", base, err)
+ }
+ round, err := json.Marshal(v)
+ if err != nil {
+ t.Fatalf("re-marshal %s: %v", base, err)
+ }
+ if ok, ga, gw := equalJSON(round, want); !ok {
+ t.Fatalf("%s round-trip mismatch\n got: %s\nwant: %s", base, ga, gw)
+ }
+ }
+}
+
+func TestJSONGolden_ContentBlocks(t *testing.T) {
+ t.Parallel()
+ t.Run("content_text", runGolden(
+ func() ContentBlock { return TextBlock("What's the weather like today?") },
+ ))
+ t.Run("content_image", runGolden(
+ func() ContentBlock { return ImageBlock("iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...", "image/png") },
+ ))
+ t.Run("content_audio", runGolden(
+ func() ContentBlock { return AudioBlock("UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB...", "audio/wav") },
+ ))
+ t.Run("content_resource_text", runGolden(
+ func() ContentBlock {
+ res := EmbeddedResourceResource{TextResourceContents: &TextResourceContents{Uri: "file:///home/user/script.py", MimeType: Ptr("text/x-python"), Text: "def hello():\n print('Hello, world!')"}}
+ return ResourceBlock(EmbeddedResource{Resource: res})
+ },
+ ))
+ t.Run("content_resource_blob", runGolden(
+ func() ContentBlock {
+ res := EmbeddedResourceResource{BlobResourceContents: &BlobResourceContents{Uri: "file:///home/user/document.pdf", MimeType: Ptr("application/pdf"), Blob: ""}}
+ return ResourceBlock(EmbeddedResource{Resource: res})
+ },
+ ))
+ t.Run("content_resource_link", runGolden(
+ func() ContentBlock {
+ mt := "application/pdf"
+ sz := 1024000
+ return ContentBlock{ResourceLink: &ContentBlockResourceLink{Type: "resource_link", Uri: "file:///home/user/document.pdf", Name: "document.pdf", MimeType: &mt, Size: &sz}}
+ },
+ func() ContentBlock {
+ cb := ResourceLinkBlock("document.pdf", "file:///home/user/document.pdf")
+ mt := "application/pdf"
+ sz := 1024000
+ cb.ResourceLink.MimeType = &mt
+ cb.ResourceLink.Size = &sz
+ return cb
+ },
+ ))
+}
+
+func TestJSONGolden_ToolCallContent(t *testing.T) {
+ t.Parallel()
+ t.Run("tool_content_content_text", runGolden(
+ func() ToolCallContent { return ToolContent(TextBlock("Analysis complete. Found 3 issues.")) },
+ ))
+ t.Run("tool_content_diff", runGolden(func() ToolCallContent {
+ old := "{\n \"debug\": false\n}"
+ return ToolDiffContent("/home/user/project/src/config.json", "{\n \"debug\": true\n}", old)
+ }))
+ t.Run("tool_content_diff_no_old", runGolden(
+ func() ToolCallContent {
+ return ToolDiffContent("/home/user/project/src/config.json", "{\n \"debug\": true\n}")
+ },
+ ))
+ t.Run("tool_content_terminal", runGolden(
+ func() ToolCallContent { return ToolTerminalRef("term_001") },
+ ))
+}
+
+func TestJSONGolden_RequestPermissionOutcome(t *testing.T) {
+ t.Parallel()
+ t.Run("permission_outcome_selected", runGolden(
+ func() RequestPermissionOutcome {
+ return RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{Outcome: "selected", OptionId: "allow-once"}}
+ },
+ func() RequestPermissionOutcome {
+ return NewRequestPermissionOutcomeSelected("allow-once")
+ },
+ ))
+ t.Run("permission_outcome_cancelled", runGolden(
+ func() RequestPermissionOutcome {
+ return RequestPermissionOutcome{Cancelled: &RequestPermissionOutcomeCancelled{Outcome: "cancelled"}}
+ },
+ func() RequestPermissionOutcome { return NewRequestPermissionOutcomeCancelled() },
+ ))
+}
+
+func TestJSONGolden_SessionUpdates(t *testing.T) {
+ t.Parallel()
+ t.Run("session_update_user_message_chunk", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{UserMessageChunk: &SessionUpdateUserMessageChunk{Content: TextBlock("What's the capital of France?")}}
+ },
+ func() SessionUpdate { return UpdateUserMessageText("What's the capital of France?") },
+ ))
+ t.Run("session_update_agent_message_chunk", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{AgentMessageChunk: &SessionUpdateAgentMessageChunk{Content: TextBlock("The capital of France is Paris.")}}
+ },
+ func() SessionUpdate { return UpdateAgentMessageText("The capital of France is Paris.") },
+ ))
+ t.Run("session_update_agent_thought_chunk", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{AgentThoughtChunk: &SessionUpdateAgentThoughtChunk{Content: TextBlock("Thinking about best approach...")}}
+ },
+ func() SessionUpdate { return UpdateAgentThoughtText("Thinking about best approach...") },
+ ))
+ t.Run("session_update_plan", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{Plan: &SessionUpdatePlan{Entries: []PlanEntry{{Content: "Check for syntax errors", Priority: PlanEntryPriorityHigh, Status: PlanEntryStatusPending}, {Content: "Identify potential type issues", Priority: PlanEntryPriorityMedium, Status: PlanEntryStatusPending}}}}
+ },
+ func() SessionUpdate {
+ return UpdatePlan(
+ PlanEntry{Content: "Check for syntax errors", Priority: PlanEntryPriorityHigh, Status: PlanEntryStatusPending},
+ PlanEntry{Content: "Identify potential type issues", Priority: PlanEntryPriorityMedium, Status: PlanEntryStatusPending},
+ )
+ },
+ ))
+ t.Run("session_update_tool_call", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{ToolCall: &SessionUpdateToolCall{ToolCallId: "call_001", Title: "Reading configuration file", Kind: ToolKindRead, Status: ToolCallStatusPending}}
+ },
+ func() SessionUpdate {
+ return StartToolCall("call_001", "Reading configuration file", WithStartKind(ToolKindRead), WithStartStatus(ToolCallStatusPending))
+ },
+ ))
+ t.Run("session_update_tool_call_read", runGolden(
+ func() SessionUpdate {
+ return StartReadToolCall("call_001", "Reading configuration file", "/home/user/project/src/config.json")
+ },
+ ))
+ t.Run("session_update_tool_call_edit", runGolden(
+ func() SessionUpdate {
+ return StartEditToolCall("call_003", "Apply edit", "/home/user/project/src/config.json", "print('hello')")
+ },
+ ))
+ t.Run("session_update_tool_call_locations_rawinput", runGolden(
+ func() SessionUpdate {
+ return StartToolCall("call_lr", "Tracking file", WithStartLocations([]ToolCallLocation{{Path: "/home/user/project/src/config.json"}}))
+ },
+ ))
+ t.Run("session_update_tool_call_update_content", runGolden(
+ func() SessionUpdate {
+ return SessionUpdate{ToolCallUpdate: &SessionUpdateToolCallUpdate{ToolCallId: "call_001", Status: Ptr(ToolCallStatusInProgress), Content: []ToolCallContent{ToolContent(TextBlock("Found 3 configuration files..."))}}}
+ },
+ func() SessionUpdate {
+ return UpdateToolCall("call_001", WithUpdateStatus(ToolCallStatusInProgress), WithUpdateContent([]ToolCallContent{ToolContent(TextBlock("Found 3 configuration files..."))}))
+ },
+ ))
+ t.Run("session_update_tool_call_update_more_fields", runGolden(
+ func() SessionUpdate {
+ return UpdateToolCall(
+ "call_010",
+ WithUpdateTitle("Processing changes"),
+ WithUpdateKind(ToolKindEdit),
+ WithUpdateStatus(ToolCallStatusCompleted),
+ WithUpdateLocations([]ToolCallLocation{{Path: "/home/user/project/src/config.json"}}),
+ WithUpdateRawInput(map[string]any{"path": "/home/user/project/src/config.json"}),
+ WithUpdateRawOutput(map[string]any{"result": "ok"}),
+ WithUpdateContent([]ToolCallContent{ToolContent(TextBlock("Edit completed."))}),
+ )
+ },
+ ))
+}
+
+func TestJSONGolden_MethodPayloads(t *testing.T) {
+ t.Parallel()
+ t.Run("initialize_request", runGolden(func() InitializeRequest {
+ return InitializeRequest{ProtocolVersion: 1, ClientCapabilities: ClientCapabilities{Fs: FileSystemCapability{ReadTextFile: true, WriteTextFile: true}}}
+ }))
+ t.Run("initialize_response", runGolden(func() InitializeResponse {
+ return InitializeResponse{ProtocolVersion: 1, AgentCapabilities: AgentCapabilities{LoadSession: true, PromptCapabilities: PromptCapabilities{Image: true, Audio: true, EmbeddedContext: true}}}
+ }))
+ t.Run("new_session_request", runGolden(func() NewSessionRequest {
+ return NewSessionRequest{
+ Cwd: "/home/user/project", McpServers: []McpServer{
+ {
+ Stdio: &Stdio{
+ Name: "filesystem",
+ Command: "/path/to/mcp-server",
+ Args: []string{"--stdio"},
+ Env: []EnvVariable{},
+ },
+ },
+ },
+ }
+ }))
+ t.Run("new_session_response", runGolden(func() NewSessionResponse { return NewSessionResponse{SessionId: "sess_abc123def456"} }))
+ t.Run("prompt_request", runGolden(func() PromptRequest {
+ return PromptRequest{SessionId: "sess_abc123def456", Prompt: []ContentBlock{TextBlock("Can you analyze this code for potential issues?"), ResourceBlock(EmbeddedResource{Resource: EmbeddedResourceResource{TextResourceContents: &TextResourceContents{Uri: "file:///home/user/project/main.py", MimeType: Ptr("text/x-python"), Text: "def process_data(items):\n for item in items:\n print(item)"}}})}}
+ }))
+ t.Run("fs_read_text_file_request", runGolden(func() ReadTextFileRequest {
+ line, limit := 10, 50
+ return ReadTextFileRequest{SessionId: "sess_abc123def456", Path: "/home/user/project/src/main.py", Line: &line, Limit: &limit}
+ }))
+ t.Run("fs_read_text_file_response", runGolden(func() ReadTextFileResponse {
+ return ReadTextFileResponse{Content: "def hello_world():\n print('Hello, world!')\n"}
+ }))
+ t.Run("fs_write_text_file_request", runGolden(func() WriteTextFileRequest {
+ return WriteTextFileRequest{SessionId: "sess_abc123def456", Path: "/home/user/project/config.json", Content: "{\n \"debug\": true,\n \"version\": \"1.0.0\"\n}"}
+ }))
+ t.Run("request_permission_request", runGolden(func() RequestPermissionRequest {
+ return RequestPermissionRequest{SessionId: "sess_abc123def456", ToolCall: ToolCallUpdate{ToolCallId: "call_001"}, Options: []PermissionOption{{OptionId: "allow-once", Name: "Allow once", Kind: PermissionOptionKindAllowOnce}, {OptionId: "reject-once", Name: "Reject", Kind: PermissionOptionKindRejectOnce}}}
+ }))
+ t.Run("request_permission_response_selected", runGolden(func() RequestPermissionResponse {
+ return RequestPermissionResponse{Outcome: RequestPermissionOutcome{Selected: &RequestPermissionOutcomeSelected{Outcome: "selected", OptionId: "allow-once"}}}
+ }))
+ t.Run("cancel_notification", runGolden(func() CancelNotification { return CancelNotification{SessionId: "sess_abc123def456"} }))
+}
diff --git a/go/testdata/json_golden/cancel_notification.json b/go/testdata/json_golden/cancel_notification.json
new file mode 100644
index 00000000..a5461d26
--- /dev/null
+++ b/go/testdata/json_golden/cancel_notification.json
@@ -0,0 +1,3 @@
+{
+ "sessionId": "sess_abc123def456"
+}
diff --git a/go/testdata/json_golden/content_audio.json b/go/testdata/json_golden/content_audio.json
new file mode 100644
index 00000000..6cd650ea
--- /dev/null
+++ b/go/testdata/json_golden/content_audio.json
@@ -0,0 +1,5 @@
+{
+ "type": "audio",
+ "mimeType": "audio/wav",
+ "data": "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB..."
+}
diff --git a/go/testdata/json_golden/content_image.json b/go/testdata/json_golden/content_image.json
new file mode 100644
index 00000000..fca8b880
--- /dev/null
+++ b/go/testdata/json_golden/content_image.json
@@ -0,0 +1,5 @@
+{
+ "type": "image",
+ "mimeType": "image/png",
+ "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
+}
diff --git a/go/testdata/json_golden/content_resource_blob.json b/go/testdata/json_golden/content_resource_blob.json
new file mode 100644
index 00000000..48325031
--- /dev/null
+++ b/go/testdata/json_golden/content_resource_blob.json
@@ -0,0 +1,8 @@
+{
+ "type": "resource",
+ "resource": {
+ "uri": "file:///home/user/document.pdf",
+ "mimeType": "application/pdf",
+ "blob": ""
+ }
+}
diff --git a/go/testdata/json_golden/content_resource_link.json b/go/testdata/json_golden/content_resource_link.json
new file mode 100644
index 00000000..4e33c1ee
--- /dev/null
+++ b/go/testdata/json_golden/content_resource_link.json
@@ -0,0 +1,7 @@
+{
+ "type": "resource_link",
+ "uri": "file:///home/user/document.pdf",
+ "name": "document.pdf",
+ "mimeType": "application/pdf",
+ "size": 1024000
+}
diff --git a/go/testdata/json_golden/content_resource_text.json b/go/testdata/json_golden/content_resource_text.json
new file mode 100644
index 00000000..f73945a3
--- /dev/null
+++ b/go/testdata/json_golden/content_resource_text.json
@@ -0,0 +1,8 @@
+{
+ "type": "resource",
+ "resource": {
+ "uri": "file:///home/user/script.py",
+ "mimeType": "text/x-python",
+ "text": "def hello():\n print('Hello, world!')"
+ }
+}
diff --git a/go/testdata/json_golden/content_text.json b/go/testdata/json_golden/content_text.json
new file mode 100644
index 00000000..63b2e85b
--- /dev/null
+++ b/go/testdata/json_golden/content_text.json
@@ -0,0 +1,4 @@
+{
+ "type": "text",
+ "text": "What's the weather like today?"
+}
diff --git a/go/testdata/json_golden/fs_read_text_file_request.json b/go/testdata/json_golden/fs_read_text_file_request.json
new file mode 100644
index 00000000..3d3ccca4
--- /dev/null
+++ b/go/testdata/json_golden/fs_read_text_file_request.json
@@ -0,0 +1,6 @@
+{
+ "sessionId": "sess_abc123def456",
+ "path": "/home/user/project/src/main.py",
+ "line": 10,
+ "limit": 50
+}
diff --git a/go/testdata/json_golden/fs_read_text_file_response.json b/go/testdata/json_golden/fs_read_text_file_response.json
new file mode 100644
index 00000000..b5dac574
--- /dev/null
+++ b/go/testdata/json_golden/fs_read_text_file_response.json
@@ -0,0 +1,3 @@
+{
+ "content": "def hello_world():\n print('Hello, world!')\n"
+}
diff --git a/go/testdata/json_golden/fs_write_text_file_request.json b/go/testdata/json_golden/fs_write_text_file_request.json
new file mode 100644
index 00000000..efbad093
--- /dev/null
+++ b/go/testdata/json_golden/fs_write_text_file_request.json
@@ -0,0 +1,5 @@
+{
+ "sessionId": "sess_abc123def456",
+ "path": "/home/user/project/config.json",
+ "content": "{\n \"debug\": true,\n \"version\": \"1.0.0\"\n}"
+}
diff --git a/go/testdata/json_golden/initialize_request.json b/go/testdata/json_golden/initialize_request.json
new file mode 100644
index 00000000..b2399094
--- /dev/null
+++ b/go/testdata/json_golden/initialize_request.json
@@ -0,0 +1,9 @@
+{
+ "protocolVersion": 1,
+ "clientCapabilities": {
+ "fs": {
+ "readTextFile": true,
+ "writeTextFile": true
+ }
+ }
+}
diff --git a/go/testdata/json_golden/initialize_response.json b/go/testdata/json_golden/initialize_response.json
new file mode 100644
index 00000000..66abb811
--- /dev/null
+++ b/go/testdata/json_golden/initialize_response.json
@@ -0,0 +1,13 @@
+{
+ "protocolVersion": 1,
+ "agentCapabilities": {
+ "loadSession": true,
+ "mcpCapabilities": {},
+ "promptCapabilities": {
+ "image": true,
+ "audio": true,
+ "embeddedContext": true
+ }
+ },
+ "authMethods": []
+}
diff --git a/go/testdata/json_golden/new_session_request.json b/go/testdata/json_golden/new_session_request.json
new file mode 100644
index 00000000..27f57c2d
--- /dev/null
+++ b/go/testdata/json_golden/new_session_request.json
@@ -0,0 +1,11 @@
+{
+ "cwd": "/home/user/project",
+ "mcpServers": [
+ {
+ "name": "filesystem",
+ "command": "/path/to/mcp-server",
+ "args": ["--stdio"],
+ "env": []
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/new_session_response.json b/go/testdata/json_golden/new_session_response.json
new file mode 100644
index 00000000..a5461d26
--- /dev/null
+++ b/go/testdata/json_golden/new_session_response.json
@@ -0,0 +1,3 @@
+{
+ "sessionId": "sess_abc123def456"
+}
diff --git a/go/testdata/json_golden/permission_outcome_cancelled.json b/go/testdata/json_golden/permission_outcome_cancelled.json
new file mode 100644
index 00000000..38f0331d
--- /dev/null
+++ b/go/testdata/json_golden/permission_outcome_cancelled.json
@@ -0,0 +1,3 @@
+{
+ "outcome": "cancelled"
+}
diff --git a/go/testdata/json_golden/permission_outcome_selected.json b/go/testdata/json_golden/permission_outcome_selected.json
new file mode 100644
index 00000000..3a194c2f
--- /dev/null
+++ b/go/testdata/json_golden/permission_outcome_selected.json
@@ -0,0 +1,4 @@
+{
+ "outcome": "selected",
+ "optionId": "allow-once"
+}
diff --git a/go/testdata/json_golden/prompt_request.json b/go/testdata/json_golden/prompt_request.json
new file mode 100644
index 00000000..816fae13
--- /dev/null
+++ b/go/testdata/json_golden/prompt_request.json
@@ -0,0 +1,17 @@
+{
+ "sessionId": "sess_abc123def456",
+ "prompt": [
+ {
+ "type": "text",
+ "text": "Can you analyze this code for potential issues?"
+ },
+ {
+ "type": "resource",
+ "resource": {
+ "uri": "file:///home/user/project/main.py",
+ "mimeType": "text/x-python",
+ "text": "def process_data(items):\n for item in items:\n print(item)"
+ }
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/request_permission_request.json b/go/testdata/json_golden/request_permission_request.json
new file mode 100644
index 00000000..1fb297f6
--- /dev/null
+++ b/go/testdata/json_golden/request_permission_request.json
@@ -0,0 +1,18 @@
+{
+ "sessionId": "sess_abc123def456",
+ "toolCall": {
+ "toolCallId": "call_001"
+ },
+ "options": [
+ {
+ "optionId": "allow-once",
+ "name": "Allow once",
+ "kind": "allow_once"
+ },
+ {
+ "optionId": "reject-once",
+ "name": "Reject",
+ "kind": "reject_once"
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/request_permission_response_selected.json b/go/testdata/json_golden/request_permission_response_selected.json
new file mode 100644
index 00000000..e29b89ba
--- /dev/null
+++ b/go/testdata/json_golden/request_permission_response_selected.json
@@ -0,0 +1,6 @@
+{
+ "outcome": {
+ "outcome": "selected",
+ "optionId": "allow-once"
+ }
+}
diff --git a/go/testdata/json_golden/session_update_agent_message_chunk.json b/go/testdata/json_golden/session_update_agent_message_chunk.json
new file mode 100644
index 00000000..7ace7edd
--- /dev/null
+++ b/go/testdata/json_golden/session_update_agent_message_chunk.json
@@ -0,0 +1,7 @@
+{
+ "sessionUpdate": "agent_message_chunk",
+ "content": {
+ "type": "text",
+ "text": "The capital of France is Paris."
+ }
+}
diff --git a/go/testdata/json_golden/session_update_agent_thought_chunk.json b/go/testdata/json_golden/session_update_agent_thought_chunk.json
new file mode 100644
index 00000000..893c13bc
--- /dev/null
+++ b/go/testdata/json_golden/session_update_agent_thought_chunk.json
@@ -0,0 +1,7 @@
+{
+ "sessionUpdate": "agent_thought_chunk",
+ "content": {
+ "type": "text",
+ "text": "Thinking about best approach..."
+ }
+}
diff --git a/go/testdata/json_golden/session_update_plan.json b/go/testdata/json_golden/session_update_plan.json
new file mode 100644
index 00000000..bad3e8a1
--- /dev/null
+++ b/go/testdata/json_golden/session_update_plan.json
@@ -0,0 +1,15 @@
+{
+ "sessionUpdate": "plan",
+ "entries": [
+ {
+ "content": "Check for syntax errors",
+ "priority": "high",
+ "status": "pending"
+ },
+ {
+ "content": "Identify potential type issues",
+ "priority": "medium",
+ "status": "pending"
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/session_update_tool_call.json b/go/testdata/json_golden/session_update_tool_call.json
new file mode 100644
index 00000000..448649d2
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call.json
@@ -0,0 +1,7 @@
+{
+ "sessionUpdate": "tool_call",
+ "toolCallId": "call_001",
+ "title": "Reading configuration file",
+ "kind": "read",
+ "status": "pending"
+}
diff --git a/go/testdata/json_golden/session_update_tool_call_edit.json b/go/testdata/json_golden/session_update_tool_call_edit.json
new file mode 100644
index 00000000..1cf0bdaf
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call_edit.json
@@ -0,0 +1,16 @@
+{
+ "sessionUpdate": "tool_call",
+ "toolCallId": "call_003",
+ "title": "Apply edit",
+ "kind": "edit",
+ "status": "pending",
+ "locations": [
+ {
+ "path": "/home/user/project/src/config.json"
+ }
+ ],
+ "rawInput": {
+ "path": "/home/user/project/src/config.json",
+ "content": "print('hello')"
+ }
+}
diff --git a/go/testdata/json_golden/session_update_tool_call_locations_rawinput.json b/go/testdata/json_golden/session_update_tool_call_locations_rawinput.json
new file mode 100644
index 00000000..a1ac3e47
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call_locations_rawinput.json
@@ -0,0 +1,13 @@
+{
+ "sessionUpdate": "tool_call",
+ "toolCallId": "call_lr",
+ "title": "Tracking file",
+ "locations": [
+ {
+ "path": "/home/user/project/src/config.json"
+ }
+ ],
+ "rawInput": {
+ "path": "/home/user/project/src/config.json"
+ }
+}
diff --git a/go/testdata/json_golden/session_update_tool_call_read.json b/go/testdata/json_golden/session_update_tool_call_read.json
new file mode 100644
index 00000000..d533afb6
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call_read.json
@@ -0,0 +1,15 @@
+{
+ "sessionUpdate": "tool_call",
+ "toolCallId": "call_001",
+ "title": "Reading configuration file",
+ "kind": "read",
+ "status": "pending",
+ "locations": [
+ {
+ "path": "/home/user/project/src/config.json"
+ }
+ ],
+ "rawInput": {
+ "path": "/home/user/project/src/config.json"
+ }
+}
diff --git a/go/testdata/json_golden/session_update_tool_call_update_content.json b/go/testdata/json_golden/session_update_tool_call_update_content.json
new file mode 100644
index 00000000..e28b4614
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call_update_content.json
@@ -0,0 +1,14 @@
+{
+ "sessionUpdate": "tool_call_update",
+ "toolCallId": "call_001",
+ "status": "in_progress",
+ "content": [
+ {
+ "type": "content",
+ "content": {
+ "type": "text",
+ "text": "Found 3 configuration files..."
+ }
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/session_update_tool_call_update_more_fields.json b/go/testdata/json_golden/session_update_tool_call_update_more_fields.json
new file mode 100644
index 00000000..d5af3359
--- /dev/null
+++ b/go/testdata/json_golden/session_update_tool_call_update_more_fields.json
@@ -0,0 +1,27 @@
+{
+ "sessionUpdate": "tool_call_update",
+ "toolCallId": "call_010",
+ "title": "Processing changes",
+ "kind": "edit",
+ "status": "completed",
+ "locations": [
+ {
+ "path": "/home/user/project/src/config.json"
+ }
+ ],
+ "rawInput": {
+ "path": "/home/user/project/src/config.json"
+ },
+ "rawOutput": {
+ "result": "ok"
+ },
+ "content": [
+ {
+ "type": "content",
+ "content": {
+ "type": "text",
+ "text": "Edit completed."
+ }
+ }
+ ]
+}
diff --git a/go/testdata/json_golden/session_update_user_message_chunk.json b/go/testdata/json_golden/session_update_user_message_chunk.json
new file mode 100644
index 00000000..8ca73e7b
--- /dev/null
+++ b/go/testdata/json_golden/session_update_user_message_chunk.json
@@ -0,0 +1,7 @@
+{
+ "sessionUpdate": "user_message_chunk",
+ "content": {
+ "type": "text",
+ "text": "What's the capital of France?"
+ }
+}
diff --git a/go/testdata/json_golden/tool_content_content_text.json b/go/testdata/json_golden/tool_content_content_text.json
new file mode 100644
index 00000000..bf3b6f77
--- /dev/null
+++ b/go/testdata/json_golden/tool_content_content_text.json
@@ -0,0 +1,7 @@
+{
+ "type": "content",
+ "content": {
+ "type": "text",
+ "text": "Analysis complete. Found 3 issues."
+ }
+}
diff --git a/go/testdata/json_golden/tool_content_diff.json b/go/testdata/json_golden/tool_content_diff.json
new file mode 100644
index 00000000..98482cb7
--- /dev/null
+++ b/go/testdata/json_golden/tool_content_diff.json
@@ -0,0 +1,6 @@
+{
+ "type": "diff",
+ "path": "/home/user/project/src/config.json",
+ "oldText": "{\n \"debug\": false\n}",
+ "newText": "{\n \"debug\": true\n}"
+}
diff --git a/go/testdata/json_golden/tool_content_diff_no_old.json b/go/testdata/json_golden/tool_content_diff_no_old.json
new file mode 100644
index 00000000..c044187f
--- /dev/null
+++ b/go/testdata/json_golden/tool_content_diff_no_old.json
@@ -0,0 +1,5 @@
+{
+ "type": "diff",
+ "path": "/home/user/project/src/config.json",
+ "newText": "{\n \"debug\": true\n}"
+}
diff --git a/go/testdata/json_golden/tool_content_terminal.json b/go/testdata/json_golden/tool_content_terminal.json
new file mode 100644
index 00000000..fd0c6762
--- /dev/null
+++ b/go/testdata/json_golden/tool_content_terminal.json
@@ -0,0 +1,4 @@
+{
+ "type": "terminal",
+ "terminalId": "term_001"
+}
diff --git a/go/types_gen.go b/go/types_gen.go
new file mode 100644
index 00000000..6e91ba79
--- /dev/null
+++ b/go/types_gen.go
@@ -0,0 +1,3435 @@
+// Code generated by acp-go-generator; DO NOT EDIT.
+
+package acp
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+)
+
+// Capabilities supported by the agent. Advertised during initialization to inform the client about available features and content types. See protocol docs: [Agent Capabilities](https://agentclientprotocol.com/protocol/initialization#agent-capabilities)
+type AgentCapabilities struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Whether the agent supports 'session/load'.
+ //
+ // Defaults to false if unset.
+ LoadSession bool `json:"loadSession,omitempty"`
+ // MCP capabilities supported by the agent.
+ //
+ // Defaults to {"http":false,"sse":false} if unset.
+ McpCapabilities McpCapabilities `json:"mcpCapabilities,omitempty"`
+ // Prompt capabilities supported by the agent.
+ //
+ // Defaults to {"audio":false,"embeddedContext":false,"image":false} if unset.
+ PromptCapabilities PromptCapabilities `json:"promptCapabilities,omitempty"`
+}
+
+func (v AgentCapabilities) MarshalJSON() ([]byte, error) {
+ type Alias AgentCapabilities
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *AgentCapabilities) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias AgentCapabilities
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["loadSession"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.LoadSession)
+ }
+ }
+ {
+ _rm, _ok := m["mcpCapabilities"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("{\"http\":false,\"sse\":false}"), &a.McpCapabilities)
+ }
+ }
+ {
+ _rm, _ok := m["promptCapabilities"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("{\"audio\":false,\"embeddedContext\":false,\"image\":false}"), &a.PromptCapabilities)
+ }
+ }
+ *v = AgentCapabilities(a)
+ return nil
+}
+
+// All possible notifications that an agent can send to a client. This enum is used internally for routing RPC notifications. You typically won't need to use this directly - use the notification methods on the ['Client'] trait instead. Notifications do not expect a response.
+type AgentNotification struct {
+ SessionNotification *SessionNotification `json:"-"`
+}
+
+func (u *AgentNotification) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v SessionNotification
+ if json.Unmarshal(b, &v) == nil {
+ u.SessionNotification = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u AgentNotification) MarshalJSON() ([]byte, error) {
+ if u.SessionNotification != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.SessionNotification)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// All possible requests that an agent can send to a client. This enum is used internally for routing RPC requests. You typically won't need to use this directly - instead, use the methods on the ['Client'] trait. This enum encompasses all method calls from agent to client.
+type AgentRequest struct {
+ WriteTextFileRequest *WriteTextFileRequest `json:"-"`
+ ReadTextFileRequest *ReadTextFileRequest `json:"-"`
+ RequestPermissionRequest *RequestPermissionRequest `json:"-"`
+ CreateTerminalRequest *CreateTerminalRequest `json:"-"`
+ TerminalOutputRequest *TerminalOutputRequest `json:"-"`
+ ReleaseTerminalRequest *ReleaseTerminalRequest `json:"-"`
+ WaitForTerminalExitRequest *WaitForTerminalExitRequest `json:"-"`
+ KillTerminalCommandRequest *KillTerminalCommandRequest `json:"-"`
+}
+
+func (u *AgentRequest) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v WriteTextFileRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.WriteTextFileRequest = &v
+ return nil
+ }
+ }
+ {
+ var v ReadTextFileRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.ReadTextFileRequest = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.RequestPermissionRequest = &v
+ return nil
+ }
+ }
+ {
+ var v CreateTerminalRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.CreateTerminalRequest = &v
+ return nil
+ }
+ }
+ {
+ var v TerminalOutputRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.TerminalOutputRequest = &v
+ return nil
+ }
+ }
+ {
+ var v ReleaseTerminalRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.ReleaseTerminalRequest = &v
+ return nil
+ }
+ }
+ {
+ var v WaitForTerminalExitRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.WaitForTerminalExitRequest = &v
+ return nil
+ }
+ }
+ {
+ var v KillTerminalCommandRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.KillTerminalCommandRequest = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u AgentRequest) MarshalJSON() ([]byte, error) {
+ if u.WriteTextFileRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.WriteTextFileRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.ReadTextFileRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ReadTextFileRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.RequestPermissionRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.RequestPermissionRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.CreateTerminalRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.CreateTerminalRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.TerminalOutputRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.TerminalOutputRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.ReleaseTerminalRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ReleaseTerminalRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.WaitForTerminalExitRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.WaitForTerminalExitRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.KillTerminalCommandRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.KillTerminalCommandRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// All possible responses that an agent can send to a client. This enum is used internally for routing RPC responses. You typically won't need to use this directly - the responses are handled automatically by the connection. These are responses to the corresponding 'ClientRequest' variants.
+type AgentResponse struct {
+ InitializeResponse *InitializeResponse `json:"-"`
+ AuthenticateResponse *AuthenticateResponse `json:"-"`
+ NewSessionResponse *NewSessionResponse `json:"-"`
+ LoadSessionResponse *LoadSessionResponse `json:"-"`
+ SetSessionModeResponse *SetSessionModeResponse `json:"-"`
+ PromptResponse *PromptResponse `json:"-"`
+ SetSessionModelResponse *SetSessionModelResponse `json:"-"`
+}
+
+func (u *AgentResponse) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v InitializeResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.InitializeResponse = &v
+ return nil
+ }
+ }
+ {
+ var v AuthenticateResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.AuthenticateResponse = &v
+ return nil
+ }
+ }
+ {
+ var v NewSessionResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.NewSessionResponse = &v
+ return nil
+ }
+ }
+ {
+ var v LoadSessionResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.LoadSessionResponse = &v
+ return nil
+ }
+ }
+ {
+ var v SetSessionModeResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.SetSessionModeResponse = &v
+ return nil
+ }
+ }
+ {
+ var v PromptResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.PromptResponse = &v
+ return nil
+ }
+ }
+ {
+ var v SetSessionModelResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.SetSessionModelResponse = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u AgentResponse) MarshalJSON() ([]byte, error) {
+ if u.InitializeResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.InitializeResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.AuthenticateResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.AuthenticateResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.NewSessionResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.NewSessionResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.LoadSessionResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.LoadSessionResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.SetSessionModeResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.SetSessionModeResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.PromptResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.PromptResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.SetSessionModelResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.SetSessionModelResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
+type Annotations struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Audience []Role `json:"audience,omitempty"`
+ LastModified *string `json:"lastModified,omitempty"`
+ Priority *float64 `json:"priority,omitempty"`
+}
+
+// Audio provided to or from an LLM.
+type AudioContent struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Data string `json:"data"`
+ MimeType string `json:"mimeType"`
+}
+
+// Describes an available authentication method.
+type AuthMethod struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Optional description providing more details about this authentication method.
+ Description *string `json:"description,omitempty"`
+ // Unique identifier for this authentication method.
+ Id AuthMethodId `json:"id"`
+ // Human-readable name of the authentication method.
+ Name string `json:"name"`
+}
+
+// Unique identifier for an authentication method.
+type AuthMethodId string
+
+// Request parameters for the authenticate method. Specifies which authentication method to use.
+type AuthenticateRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The ID of the authentication method to use. Must be one of the methods advertised in the initialize response.
+ MethodId AuthMethodId `json:"methodId"`
+}
+
+func (v *AuthenticateRequest) Validate() error {
+ return nil
+}
+
+// Response to authenticate method
+type AuthenticateResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+}
+
+func (v *AuthenticateResponse) Validate() error {
+ return nil
+}
+
+// Information about a command.
+type AvailableCommand struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Human-readable description of what the command does.
+ Description string `json:"description"`
+ // Input for the command if required
+ Input *AvailableCommandInput `json:"input,omitempty"`
+ // Command name (e.g., 'create_plan', 'research_codebase').
+ Name string `json:"name"`
+}
+
+// The input specification for a command.
+// All text that was typed after the command name is provided as input.
+type UnstructuredCommandInput struct {
+ // A hint to display when the input hasn't been provided yet
+ Hint string `json:"hint"`
+}
+
+type AvailableCommandInput struct {
+ UnstructuredCommandInput *UnstructuredCommandInput `json:"-"`
+}
+
+func (u *AvailableCommandInput) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v UnstructuredCommandInput
+ var match bool = true
+ if _, ok := m["hint"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.UnstructuredCommandInput = &v
+ return nil
+ }
+ }
+ {
+ var v UnstructuredCommandInput
+ if json.Unmarshal(b, &v) == nil {
+ u.UnstructuredCommandInput = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u AvailableCommandInput) MarshalJSON() ([]byte, error) {
+ if u.UnstructuredCommandInput != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.UnstructuredCommandInput)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// Binary resource contents.
+type BlobResourceContents struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Blob string `json:"blob"`
+ MimeType *string `json:"mimeType,omitempty"`
+ Uri string `json:"uri"`
+}
+
+// Notification to cancel ongoing operations for a session. See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)
+type CancelNotification struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The ID of the session to cancel operations for.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *CancelNotification) Validate() error {
+ return nil
+}
+
+// Capabilities supported by the client. Advertised during initialization to inform the agent about available features and methods. See protocol docs: [Client Capabilities](https://agentclientprotocol.com/protocol/initialization#client-capabilities)
+type ClientCapabilities struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // File system capabilities supported by the client. Determines which file operations the agent can request.
+ //
+ // Defaults to {"readTextFile":false,"writeTextFile":false} if unset.
+ Fs FileSystemCapability `json:"fs,omitempty"`
+ // Whether the Client support all 'terminal/*' methods.
+ //
+ // Defaults to false if unset.
+ Terminal bool `json:"terminal,omitempty"`
+}
+
+func (v ClientCapabilities) MarshalJSON() ([]byte, error) {
+ type Alias ClientCapabilities
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *ClientCapabilities) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias ClientCapabilities
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["fs"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("{\"readTextFile\":false,\"writeTextFile\":false}"), &a.Fs)
+ }
+ }
+ {
+ _rm, _ok := m["terminal"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.Terminal)
+ }
+ }
+ *v = ClientCapabilities(a)
+ return nil
+}
+
+// All possible notifications that a client can send to an agent. This enum is used internally for routing RPC notifications. You typically won't need to use this directly - use the notification methods on the ['Agent'] trait instead. Notifications do not expect a response.
+type ClientNotification struct {
+ CancelNotification *CancelNotification `json:"-"`
+}
+
+func (u *ClientNotification) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v CancelNotification
+ if json.Unmarshal(b, &v) == nil {
+ u.CancelNotification = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u ClientNotification) MarshalJSON() ([]byte, error) {
+ if u.CancelNotification != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.CancelNotification)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// All possible requests that a client can send to an agent. This enum is used internally for routing RPC requests. You typically won't need to use this directly - instead, use the methods on the ['Agent'] trait. This enum encompasses all method calls from client to agent.
+type ClientRequest struct {
+ InitializeRequest *InitializeRequest `json:"-"`
+ AuthenticateRequest *AuthenticateRequest `json:"-"`
+ NewSessionRequest *NewSessionRequest `json:"-"`
+ LoadSessionRequest *LoadSessionRequest `json:"-"`
+ SetSessionModeRequest *SetSessionModeRequest `json:"-"`
+ PromptRequest *PromptRequest `json:"-"`
+ SetSessionModelRequest *SetSessionModelRequest `json:"-"`
+}
+
+func (u *ClientRequest) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v InitializeRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.InitializeRequest = &v
+ return nil
+ }
+ }
+ {
+ var v AuthenticateRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.AuthenticateRequest = &v
+ return nil
+ }
+ }
+ {
+ var v NewSessionRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.NewSessionRequest = &v
+ return nil
+ }
+ }
+ {
+ var v LoadSessionRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.LoadSessionRequest = &v
+ return nil
+ }
+ }
+ {
+ var v SetSessionModeRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.SetSessionModeRequest = &v
+ return nil
+ }
+ }
+ {
+ var v PromptRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.PromptRequest = &v
+ return nil
+ }
+ }
+ {
+ var v SetSessionModelRequest
+ if json.Unmarshal(b, &v) == nil {
+ u.SetSessionModelRequest = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u ClientRequest) MarshalJSON() ([]byte, error) {
+ if u.InitializeRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.InitializeRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.AuthenticateRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.AuthenticateRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.NewSessionRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.NewSessionRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.LoadSessionRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.LoadSessionRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.SetSessionModeRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.SetSessionModeRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.PromptRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.PromptRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.SetSessionModelRequest != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.SetSessionModelRequest)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// All possible responses that a client can send to an agent. This enum is used internally for routing RPC responses. You typically won't need to use this directly - the responses are handled automatically by the connection. These are responses to the corresponding 'AgentRequest' variants.
+type ClientResponse struct {
+ WriteTextFileResponse *WriteTextFileResponse `json:"-"`
+ ReadTextFileResponse *ReadTextFileResponse `json:"-"`
+ RequestPermissionResponse *RequestPermissionResponse `json:"-"`
+ CreateTerminalResponse *CreateTerminalResponse `json:"-"`
+ TerminalOutputResponse *TerminalOutputResponse `json:"-"`
+ ReleaseTerminalResponse *ReleaseTerminalResponse `json:"-"`
+ WaitForTerminalExitResponse *WaitForTerminalExitResponse `json:"-"`
+ KillTerminalCommandResponse *KillTerminalCommandResponse `json:"-"`
+}
+
+func (u *ClientResponse) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var v WriteTextFileResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.WriteTextFileResponse = &v
+ return nil
+ }
+ }
+ {
+ var v ReadTextFileResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.ReadTextFileResponse = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.RequestPermissionResponse = &v
+ return nil
+ }
+ }
+ {
+ var v CreateTerminalResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.CreateTerminalResponse = &v
+ return nil
+ }
+ }
+ {
+ var v TerminalOutputResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.TerminalOutputResponse = &v
+ return nil
+ }
+ }
+ {
+ var v ReleaseTerminalResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.ReleaseTerminalResponse = &v
+ return nil
+ }
+ }
+ {
+ var v WaitForTerminalExitResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.WaitForTerminalExitResponse = &v
+ return nil
+ }
+ }
+ {
+ var v KillTerminalCommandResponse
+ if json.Unmarshal(b, &v) == nil {
+ u.KillTerminalCommandResponse = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u ClientResponse) MarshalJSON() ([]byte, error) {
+ if u.WriteTextFileResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.WriteTextFileResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.ReadTextFileResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ReadTextFileResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.RequestPermissionResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.RequestPermissionResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.CreateTerminalResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.CreateTerminalResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.TerminalOutputResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.TerminalOutputResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.ReleaseTerminalResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ReleaseTerminalResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.WaitForTerminalExitResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.WaitForTerminalExitResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.KillTerminalCommandResponse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.KillTerminalCommandResponse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// Content blocks represent displayable information in the Agent Client Protocol. They provide a structured way to handle various types of user-facing content—whether it's text from language models, images for analysis, or embedded resources for context. Content blocks appear in: - User prompts sent via 'session/prompt' - Language model output streamed through 'session/update' notifications - Progress updates and results from tool calls This structure is compatible with the Model Context Protocol (MCP), enabling agents to seamlessly forward content from MCP tool outputs without transformation. See protocol docs: [Content](https://agentclientprotocol.com/protocol/content)
+// Plain text content All agents MUST support text content blocks in prompts.
+type ContentBlockText struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Text string `json:"text"`
+ Type string `json:"type"`
+}
+
+// Images for visual context or analysis. Requires the 'image' prompt capability when included in prompts.
+type ContentBlockImage struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Data string `json:"data"`
+ MimeType string `json:"mimeType"`
+ Type string `json:"type"`
+ Uri *string `json:"uri,omitempty"`
+}
+
+// Audio data for transcription or analysis. Requires the 'audio' prompt capability when included in prompts.
+type ContentBlockAudio struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Data string `json:"data"`
+ MimeType string `json:"mimeType"`
+ Type string `json:"type"`
+}
+
+// References to resources that the agent can access. All agents MUST support resource links in prompts.
+type ContentBlockResourceLink struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Description *string `json:"description,omitempty"`
+ MimeType *string `json:"mimeType,omitempty"`
+ Name string `json:"name"`
+ Size *int `json:"size,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Type string `json:"type"`
+ Uri string `json:"uri"`
+}
+
+// Complete resource contents embedded directly in the message. Preferred for including context as it avoids extra round-trips. Requires the 'embeddedContext' prompt capability when included in prompts.
+type ContentBlockResource struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Resource EmbeddedResourceResource `json:"resource"`
+ Type string `json:"type"`
+}
+
+type ContentBlock struct {
+ Text *ContentBlockText `json:"-"`
+ Image *ContentBlockImage `json:"-"`
+ Audio *ContentBlockAudio `json:"-"`
+ ResourceLink *ContentBlockResourceLink `json:"-"`
+ Resource *ContentBlockResource `json:"-"`
+}
+
+func (u *ContentBlock) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var disc string
+ if v, ok := m["type"]; ok {
+ json.Unmarshal(v, &disc)
+ }
+ switch disc {
+ case "text":
+ var v ContentBlockText
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Text = &v
+ return nil
+ case "image":
+ var v ContentBlockImage
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Image = &v
+ return nil
+ case "audio":
+ var v ContentBlockAudio
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Audio = &v
+ return nil
+ case "resource_link":
+ var v ContentBlockResourceLink
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ResourceLink = &v
+ return nil
+ case "resource":
+ var v ContentBlockResource
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Resource = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockText
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["text"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Text = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockImage
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["data"]; !ok {
+ match = false
+ }
+ if _, ok := m["mimeType"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Image = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockAudio
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["data"]; !ok {
+ match = false
+ }
+ if _, ok := m["mimeType"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Audio = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockResourceLink
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["name"]; !ok {
+ match = false
+ }
+ if _, ok := m["uri"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ResourceLink = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockResource
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["resource"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Resource = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockText
+ if json.Unmarshal(b, &v) == nil {
+ u.Text = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockImage
+ if json.Unmarshal(b, &v) == nil {
+ u.Image = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockAudio
+ if json.Unmarshal(b, &v) == nil {
+ u.Audio = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockResourceLink
+ if json.Unmarshal(b, &v) == nil {
+ u.ResourceLink = &v
+ return nil
+ }
+ }
+ {
+ var v ContentBlockResource
+ if json.Unmarshal(b, &v) == nil {
+ u.Resource = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u ContentBlock) MarshalJSON() ([]byte, error) {
+ if u.Text != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Text)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "text"
+ {
+ var nm map[string]any
+ nm = make(map[string]any)
+ nm["type"] = "text"
+ nm["text"] = m["text"]
+ return json.Marshal(nm)
+ }
+ }
+ if u.Image != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Image)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "image"
+ {
+ var nm map[string]any
+ nm = make(map[string]any)
+ nm["type"] = "image"
+ nm["data"] = m["data"]
+ nm["mimeType"] = m["mimeType"]
+ if _v, _ok := m["uri"]; _ok {
+ nm["uri"] = _v
+ }
+ return json.Marshal(nm)
+ }
+ }
+ if u.Audio != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Audio)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "audio"
+ {
+ var nm map[string]any
+ nm = make(map[string]any)
+ nm["type"] = "audio"
+ nm["data"] = m["data"]
+ nm["mimeType"] = m["mimeType"]
+ return json.Marshal(nm)
+ }
+ }
+ if u.ResourceLink != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ResourceLink)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "resource_link"
+ {
+ var nm map[string]any
+ nm = make(map[string]any)
+ nm["type"] = "resource_link"
+ nm["name"] = m["name"]
+ nm["uri"] = m["uri"]
+ if v1, ok1 := m["description"]; ok1 {
+ nm["description"] = v1
+ }
+ if v2, ok2 := m["mimeType"]; ok2 {
+ nm["mimeType"] = v2
+ }
+ if v3, ok3 := m["size"]; ok3 {
+ nm["size"] = v3
+ }
+ if v4, ok4 := m["title"]; ok4 {
+ nm["title"] = v4
+ }
+ return json.Marshal(nm)
+ }
+ }
+ if u.Resource != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Resource)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "resource"
+ {
+ var nm map[string]any
+ nm = make(map[string]any)
+ nm["type"] = "resource"
+ nm["resource"] = m["resource"]
+ return json.Marshal(nm)
+ }
+ }
+ return []byte{}, nil
+}
+
+func (u *ContentBlock) Validate() error {
+ var count int
+ if u.Text != nil {
+ count++
+ }
+ if u.Image != nil {
+ count++
+ }
+ if u.Audio != nil {
+ count++
+ }
+ if u.ResourceLink != nil {
+ count++
+ }
+ if u.Resource != nil {
+ count++
+ }
+ if count != 1 {
+ return errors.New("ContentBlock must have exactly one variant set")
+ }
+ return nil
+}
+
+// Request to create a new terminal and execute a command.
+type CreateTerminalRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Array of command arguments.
+ Args []string `json:"args,omitempty"`
+ // The command to execute.
+ Command string `json:"command"`
+ // Working directory for the command (absolute path).
+ Cwd *string `json:"cwd,omitempty"`
+ // Environment variables for the command.
+ Env []EnvVariable `json:"env,omitempty"`
+ // Maximum number of output bytes to retain. When the limit is exceeded, the Client truncates from the beginning of the output to stay within the limit. The Client MUST ensure truncation happens at a character boundary to maintain valid string output, even if this means the retained output is slightly less than the specified limit.
+ OutputByteLimit *int `json:"outputByteLimit,omitempty"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *CreateTerminalRequest) Validate() error {
+ if v.Command == "" {
+ return fmt.Errorf("command is required")
+ }
+ return nil
+}
+
+// Response containing the ID of the created terminal.
+type CreateTerminalResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The unique identifier for the created terminal.
+ TerminalId string `json:"terminalId"`
+}
+
+func (v *CreateTerminalResponse) Validate() error {
+ if v.TerminalId == "" {
+ return fmt.Errorf("terminalId is required")
+ }
+ return nil
+}
+
+// The contents of a resource, embedded into a prompt or tool call result.
+type EmbeddedResource struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Resource EmbeddedResourceResource `json:"resource"`
+}
+
+// Resource content that can be embedded in a message.
+type EmbeddedResourceResource struct {
+ TextResourceContents *TextResourceContents `json:"-"`
+ BlobResourceContents *BlobResourceContents `json:"-"`
+}
+
+func (u *EmbeddedResourceResource) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ if _, ok := m["text"]; ok {
+ var v TextResourceContents
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.TextResourceContents = &v
+ return nil
+ }
+ if _, ok := m["blob"]; ok {
+ var v BlobResourceContents
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.BlobResourceContents = &v
+ return nil
+ }
+ {
+ var v TextResourceContents
+ if json.Unmarshal(b, &v) == nil {
+ u.TextResourceContents = &v
+ return nil
+ }
+ }
+ {
+ var v BlobResourceContents
+ if json.Unmarshal(b, &v) == nil {
+ u.BlobResourceContents = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u EmbeddedResourceResource) MarshalJSON() ([]byte, error) {
+ if u.TextResourceContents != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.TextResourceContents)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ if u.BlobResourceContents != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.BlobResourceContents)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// An environment variable to set when launching an MCP server.
+type EnvVariable struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The name of the environment variable.
+ Name string `json:"name"`
+ // The value to set for the environment variable.
+ Value string `json:"value"`
+}
+
+// File system capabilities that a client may support. See protocol docs: [FileSystem](https://agentclientprotocol.com/protocol/initialization#filesystem)
+type FileSystemCapability struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Whether the Client supports 'fs/read_text_file' requests.
+ //
+ // Defaults to false if unset.
+ ReadTextFile bool `json:"readTextFile,omitempty"`
+ // Whether the Client supports 'fs/write_text_file' requests.
+ //
+ // Defaults to false if unset.
+ WriteTextFile bool `json:"writeTextFile,omitempty"`
+}
+
+func (v FileSystemCapability) MarshalJSON() ([]byte, error) {
+ type Alias FileSystemCapability
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *FileSystemCapability) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias FileSystemCapability
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["readTextFile"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.ReadTextFile)
+ }
+ }
+ {
+ _rm, _ok := m["writeTextFile"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.WriteTextFile)
+ }
+ }
+ *v = FileSystemCapability(a)
+ return nil
+}
+
+// An HTTP header to set when making requests to the MCP server.
+type HttpHeader struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The name of the HTTP header.
+ Name string `json:"name"`
+ // The value to set for the HTTP header.
+ Value string `json:"value"`
+}
+
+// An image provided to or from an LLM.
+type ImageContent struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Data string `json:"data"`
+ MimeType string `json:"mimeType"`
+ Uri *string `json:"uri,omitempty"`
+}
+
+// Request parameters for the initialize method. Sent by the client to establish connection and negotiate capabilities. See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)
+type InitializeRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Capabilities supported by the client.
+ //
+ // Defaults to {"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false} if unset.
+ ClientCapabilities ClientCapabilities `json:"clientCapabilities,omitempty"`
+ // The latest protocol version supported by the client.
+ ProtocolVersion ProtocolVersion `json:"protocolVersion"`
+}
+
+func (v InitializeRequest) MarshalJSON() ([]byte, error) {
+ type Alias InitializeRequest
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *InitializeRequest) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias InitializeRequest
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["clientCapabilities"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("{\"fs\":{\"readTextFile\":false,\"writeTextFile\":false},\"terminal\":false}"), &a.ClientCapabilities)
+ }
+ }
+ *v = InitializeRequest(a)
+ return nil
+}
+
+func (v *InitializeRequest) Validate() error {
+ return nil
+}
+
+// Response from the initialize method. Contains the negotiated protocol version and agent capabilities. See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)
+type InitializeResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Capabilities supported by the agent.
+ //
+ // Defaults to {"loadSession":false,"mcpCapabilities":{"http":false,"sse":false},"promptCapabilities":{"audio":false,"embeddedContext":false,"image":false}} if unset.
+ AgentCapabilities AgentCapabilities `json:"agentCapabilities,omitempty"`
+ // Authentication methods supported by the agent.
+ //
+ // Defaults to [] if unset.
+ AuthMethods []AuthMethod `json:"authMethods"`
+ // The protocol version the client specified if supported by the agent, or the latest protocol version supported by the agent. The client should disconnect, if it doesn't support this version.
+ ProtocolVersion ProtocolVersion `json:"protocolVersion"`
+}
+
+func (v InitializeResponse) MarshalJSON() ([]byte, error) {
+ type Alias InitializeResponse
+ var a Alias
+ a = Alias(v)
+ if a.AuthMethods == nil {
+ json.Unmarshal([]byte("[]"), &a.AuthMethods)
+ }
+ return json.Marshal(a)
+}
+
+func (v *InitializeResponse) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias InitializeResponse
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["agentCapabilities"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("{\"loadSession\":false,\"mcpCapabilities\":{\"http\":false,\"sse\":false},\"promptCapabilities\":{\"audio\":false,\"embeddedContext\":false,\"image\":false}}"), &a.AgentCapabilities)
+ }
+ }
+ {
+ _rm, _ok := m["authMethods"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("[]"), &a.AuthMethods)
+ }
+ }
+ *v = InitializeResponse(a)
+ return nil
+}
+
+func (v *InitializeResponse) Validate() error {
+ return nil
+}
+
+// Request to kill a terminal command without releasing the terminal.
+type KillTerminalCommandRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+ // The ID of the terminal to kill.
+ TerminalId string `json:"terminalId"`
+}
+
+func (v *KillTerminalCommandRequest) Validate() error {
+ if v.TerminalId == "" {
+ return fmt.Errorf("terminalId is required")
+ }
+ return nil
+}
+
+// Response to terminal/kill command method
+type KillTerminalCommandResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+}
+
+func (v *KillTerminalCommandResponse) Validate() error {
+ return nil
+}
+
+// Request parameters for loading an existing session. Only available if the Agent supports the 'loadSession' capability. See protocol docs: [Loading Sessions](https://agentclientprotocol.com/protocol/session-setup#loading-sessions)
+type LoadSessionRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The working directory for this session.
+ Cwd string `json:"cwd"`
+ // List of MCP servers to connect to for this session.
+ McpServers []McpServer `json:"mcpServers"`
+ // The ID of the session to load.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *LoadSessionRequest) Validate() error {
+ if v.Cwd == "" {
+ return fmt.Errorf("cwd is required")
+ }
+ if v.McpServers == nil {
+ return fmt.Errorf("mcpServers is required")
+ }
+ return nil
+}
+
+// Response from loading an existing session.
+type LoadSessionResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. Initial model state if supported by the Agent
+ Models *SessionModelState `json:"models,omitempty"`
+ // Initial mode state if supported by the Agent See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes)
+ Modes *SessionModeState `json:"modes,omitempty"`
+}
+
+func (v *LoadSessionResponse) Validate() error {
+ return nil
+}
+
+// MCP capabilities supported by the agent
+type McpCapabilities struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Agent supports ['McpServer::Http'].
+ //
+ // Defaults to false if unset.
+ Http bool `json:"http,omitempty"`
+ // Agent supports ['McpServer::Sse'].
+ //
+ // Defaults to false if unset.
+ Sse bool `json:"sse,omitempty"`
+}
+
+func (v McpCapabilities) MarshalJSON() ([]byte, error) {
+ type Alias McpCapabilities
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *McpCapabilities) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias McpCapabilities
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["http"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.Http)
+ }
+ }
+ {
+ _rm, _ok := m["sse"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.Sse)
+ }
+ }
+ *v = McpCapabilities(a)
+ return nil
+}
+
+// Configuration for connecting to an MCP (Model Context Protocol) server. MCP servers provide tools and context that the agent can use when processing prompts. See protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers)
+// HTTP transport configuration Only available when the Agent capabilities indicate 'mcp_capabilities.http' is 'true'.
+type McpServerHttp struct {
+ // HTTP headers to set when making requests to the MCP server.
+ Headers []HttpHeader `json:"headers"`
+ // Human-readable name identifying this MCP server.
+ Name string `json:"name"`
+ Type string `json:"type"`
+ // URL to the MCP server.
+ Url string `json:"url"`
+}
+
+// SSE transport configuration Only available when the Agent capabilities indicate 'mcp_capabilities.sse' is 'true'.
+type McpServerSse struct {
+ // HTTP headers to set when making requests to the MCP server.
+ Headers []HttpHeader `json:"headers"`
+ // Human-readable name identifying this MCP server.
+ Name string `json:"name"`
+ Type string `json:"type"`
+ // URL to the MCP server.
+ Url string `json:"url"`
+}
+
+// Stdio transport configuration All Agents MUST support this transport.
+type Stdio struct {
+ // Command-line arguments to pass to the MCP server.
+ Args []string `json:"args"`
+ // Path to the MCP server executable.
+ Command string `json:"command"`
+ // Environment variables to set when launching the MCP server.
+ Env []EnvVariable `json:"env"`
+ // Human-readable name identifying this MCP server.
+ Name string `json:"name"`
+}
+
+type McpServer struct {
+ Http *McpServerHttp `json:"-"`
+ Sse *McpServerSse `json:"-"`
+ Stdio *Stdio `json:"-"`
+}
+
+func (u *McpServer) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var disc string
+ if v, ok := m["type"]; ok {
+ json.Unmarshal(v, &disc)
+ }
+ switch disc {
+ case "http":
+ var v McpServerHttp
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Http = &v
+ return nil
+ case "sse":
+ var v McpServerSse
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Sse = &v
+ return nil
+ }
+ }
+ {
+ var v McpServerHttp
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["name"]; !ok {
+ match = false
+ }
+ if _, ok := m["url"]; !ok {
+ match = false
+ }
+ if _, ok := m["headers"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Http = &v
+ return nil
+ }
+ }
+ {
+ var v McpServerSse
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["name"]; !ok {
+ match = false
+ }
+ if _, ok := m["url"]; !ok {
+ match = false
+ }
+ if _, ok := m["headers"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Sse = &v
+ return nil
+ }
+ }
+ {
+ var v Stdio
+ var match bool = true
+ if _, ok := m["name"]; !ok {
+ match = false
+ }
+ if _, ok := m["command"]; !ok {
+ match = false
+ }
+ if _, ok := m["args"]; !ok {
+ match = false
+ }
+ if _, ok := m["env"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Stdio = &v
+ return nil
+ }
+ }
+ {
+ var v McpServerHttp
+ if json.Unmarshal(b, &v) == nil {
+ u.Http = &v
+ return nil
+ }
+ }
+ {
+ var v McpServerSse
+ if json.Unmarshal(b, &v) == nil {
+ u.Sse = &v
+ return nil
+ }
+ }
+ {
+ var v Stdio
+ if json.Unmarshal(b, &v) == nil {
+ u.Stdio = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u McpServer) MarshalJSON() ([]byte, error) {
+ if u.Http != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Http)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "http"
+ return json.Marshal(m)
+ }
+ if u.Sse != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Sse)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "sse"
+ return json.Marshal(m)
+ }
+ if u.Stdio != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Stdio)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+// **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. A unique identifier for a model.
+type ModelId string
+
+// **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. Information about a selectable model.
+type ModelInfo struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Optional description of the model.
+ Description *string `json:"description,omitempty"`
+ // Unique identifier for the model.
+ ModelId ModelId `json:"modelId"`
+ // Human-readable name of the model.
+ Name string `json:"name"`
+}
+
+// Request parameters for creating a new session. See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session)
+type NewSessionRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The working directory for this session. Must be an absolute path.
+ Cwd string `json:"cwd"`
+ // List of MCP (Model Context Protocol) servers the agent should connect to.
+ McpServers []McpServer `json:"mcpServers"`
+}
+
+func (v *NewSessionRequest) Validate() error {
+ if v.Cwd == "" {
+ return fmt.Errorf("cwd is required")
+ }
+ if v.McpServers == nil {
+ return fmt.Errorf("mcpServers is required")
+ }
+ return nil
+}
+
+// Response from creating a new session. See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session)
+type NewSessionResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. Initial model state if supported by the Agent
+ Models *SessionModelState `json:"models,omitempty"`
+ // Initial mode state if supported by the Agent See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes)
+ Modes *SessionModeState `json:"modes,omitempty"`
+ // Unique identifier for the created session. Used in all subsequent requests for this conversation.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *NewSessionResponse) Validate() error {
+ return nil
+}
+
+// An option presented to the user when requesting permission.
+type PermissionOption struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Hint about the nature of this permission option.
+ Kind PermissionOptionKind `json:"kind"`
+ // Human-readable label to display to the user.
+ Name string `json:"name"`
+ // Unique identifier for this permission option.
+ OptionId PermissionOptionId `json:"optionId"`
+}
+
+// Unique identifier for a permission option.
+type PermissionOptionId string
+
+// The type of permission option being presented to the user. Helps clients choose appropriate icons and UI treatment.
+type PermissionOptionKind string
+
+const (
+ PermissionOptionKindAllowOnce PermissionOptionKind = "allow_once"
+ PermissionOptionKindAllowAlways PermissionOptionKind = "allow_always"
+ PermissionOptionKindRejectOnce PermissionOptionKind = "reject_once"
+ PermissionOptionKindRejectAlways PermissionOptionKind = "reject_always"
+)
+
+// An execution plan for accomplishing complex tasks. Plans consist of multiple entries representing individual tasks or goals. Agents report plans to clients to provide visibility into their execution strategy. Plans can evolve during execution as the agent discovers new requirements or completes tasks. See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)
+type Plan struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The list of tasks to be accomplished. When updating a plan, the agent must send a complete list of all entries with their current status. The client replaces the entire plan with each update.
+ Entries []PlanEntry `json:"entries"`
+}
+
+// A single entry in the execution plan. Represents a task or goal that the assistant intends to accomplish as part of fulfilling the user's request. See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
+type PlanEntry struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Human-readable description of what this task aims to accomplish.
+ Content string `json:"content"`
+ // The relative importance of this task. Used to indicate which tasks are most critical to the overall goal.
+ Priority PlanEntryPriority `json:"priority"`
+ // Current execution status of this task.
+ Status PlanEntryStatus `json:"status"`
+}
+
+// Priority levels for plan entries. Used to indicate the relative importance or urgency of different tasks in the execution plan. See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
+type PlanEntryPriority string
+
+const (
+ PlanEntryPriorityHigh PlanEntryPriority = "high"
+ PlanEntryPriorityMedium PlanEntryPriority = "medium"
+ PlanEntryPriorityLow PlanEntryPriority = "low"
+)
+
+// Status of a plan entry in the execution flow. Tracks the lifecycle of each task from planning through completion. See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)
+type PlanEntryStatus string
+
+const (
+ PlanEntryStatusPending PlanEntryStatus = "pending"
+ PlanEntryStatusInProgress PlanEntryStatus = "in_progress"
+ PlanEntryStatusCompleted PlanEntryStatus = "completed"
+)
+
+// Prompt capabilities supported by the agent in 'session/prompt' requests. Baseline agent functionality requires support for ['ContentBlock::Text'] and ['ContentBlock::ResourceLink'] in prompt requests. Other variants must be explicitly opted in to. Capabilities for different types of content in prompt requests. Indicates which content types beyond the baseline (text and resource links) the agent can process. See protocol docs: [Prompt Capabilities](https://agentclientprotocol.com/protocol/initialization#prompt-capabilities)
+type PromptCapabilities struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Agent supports ['ContentBlock::Audio'].
+ //
+ // Defaults to false if unset.
+ Audio bool `json:"audio,omitempty"`
+ // Agent supports embedded context in 'session/prompt' requests. When enabled, the Client is allowed to include ['ContentBlock::Resource'] in prompt requests for pieces of context that are referenced in the message.
+ //
+ // Defaults to false if unset.
+ EmbeddedContext bool `json:"embeddedContext,omitempty"`
+ // Agent supports ['ContentBlock::Image'].
+ //
+ // Defaults to false if unset.
+ Image bool `json:"image,omitempty"`
+}
+
+func (v PromptCapabilities) MarshalJSON() ([]byte, error) {
+ type Alias PromptCapabilities
+ var a Alias
+ a = Alias(v)
+ return json.Marshal(a)
+}
+
+func (v *PromptCapabilities) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ type Alias PromptCapabilities
+ var a Alias
+ if err := json.Unmarshal(b, &a); err != nil {
+ return err
+ }
+ {
+ _rm, _ok := m["audio"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.Audio)
+ }
+ }
+ {
+ _rm, _ok := m["embeddedContext"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.EmbeddedContext)
+ }
+ }
+ {
+ _rm, _ok := m["image"]
+ if !_ok || (string(_rm) == "null") {
+ json.Unmarshal([]byte("false"), &a.Image)
+ }
+ }
+ *v = PromptCapabilities(a)
+ return nil
+}
+
+// Request parameters for sending a user prompt to the agent. Contains the user's message and any additional context. See protocol docs: [User Message](https://agentclientprotocol.com/protocol/prompt-turn#1-user-message)
+type PromptRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The blocks of content that compose the user's message. As a baseline, the Agent MUST support ['ContentBlock::Text'] and ['ContentBlock::ResourceLink'], while other variants are optionally enabled via ['PromptCapabilities']. The Client MUST adapt its interface according to ['PromptCapabilities']. The client MAY include referenced pieces of context as either ['ContentBlock::Resource'] or ['ContentBlock::ResourceLink']. When available, ['ContentBlock::Resource'] is preferred as it avoids extra round-trips and allows the message to include pieces of context from sources the agent may not have access to.
+ Prompt []ContentBlock `json:"prompt"`
+ // The ID of the session to send this user message to
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *PromptRequest) Validate() error {
+ if v.Prompt == nil {
+ return fmt.Errorf("prompt is required")
+ }
+ return nil
+}
+
+// Response from processing a user prompt. See protocol docs: [Check for Completion](https://agentclientprotocol.com/protocol/prompt-turn#4-check-for-completion)
+type PromptResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Indicates why the agent stopped processing the turn.
+ StopReason StopReason `json:"stopReason"`
+}
+
+func (v *PromptResponse) Validate() error {
+ return nil
+}
+
+// Protocol version identifier. This version is only bumped for breaking changes. Non-breaking changes should be introduced via capabilities.
+type ProtocolVersion int
+
+// Request to read content from a text file. Only available if the client supports the 'fs.readTextFile' capability.
+type ReadTextFileRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Maximum number of lines to read.
+ Limit *int `json:"limit,omitempty"`
+ // Line number to start reading from (1-based).
+ Line *int `json:"line,omitempty"`
+ // Absolute path to the file to read.
+ Path string `json:"path"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *ReadTextFileRequest) Validate() error {
+ if v.Path == "" {
+ return fmt.Errorf("path is required")
+ }
+ return nil
+}
+
+// Response containing the contents of a text file.
+type ReadTextFileResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Content string `json:"content"`
+}
+
+func (v *ReadTextFileResponse) Validate() error {
+ if v.Content == "" {
+ return fmt.Errorf("content is required")
+ }
+ return nil
+}
+
+// Request to release a terminal and free its resources.
+type ReleaseTerminalRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+ // The ID of the terminal to release.
+ TerminalId string `json:"terminalId"`
+}
+
+func (v *ReleaseTerminalRequest) Validate() error {
+ if v.TerminalId == "" {
+ return fmt.Errorf("terminalId is required")
+ }
+ return nil
+}
+
+// Response to terminal/release method
+type ReleaseTerminalResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+}
+
+func (v *ReleaseTerminalResponse) Validate() error {
+ return nil
+}
+
+// The outcome of a permission request.
+// The prompt turn was cancelled before the user responded. When a client sends a 'session/cancel' notification to cancel an ongoing prompt turn, it MUST respond to all pending 'session/request_permission' requests with this 'Cancelled' outcome. See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)
+type RequestPermissionOutcomeCancelled struct {
+ Outcome string `json:"outcome"`
+}
+
+// The user selected one of the provided options.
+type RequestPermissionOutcomeSelected struct {
+ // The ID of the option the user selected.
+ OptionId PermissionOptionId `json:"optionId"`
+ Outcome string `json:"outcome"`
+}
+
+type RequestPermissionOutcome struct {
+ Cancelled *RequestPermissionOutcomeCancelled `json:"-"`
+ Selected *RequestPermissionOutcomeSelected `json:"-"`
+}
+
+func (u *RequestPermissionOutcome) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var disc string
+ if v, ok := m["outcome"]; ok {
+ json.Unmarshal(v, &disc)
+ }
+ switch disc {
+ case "cancelled":
+ var v RequestPermissionOutcomeCancelled
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Cancelled = &v
+ return nil
+ case "selected":
+ var v RequestPermissionOutcomeSelected
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Selected = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionOutcomeCancelled
+ var match bool = true
+ if _, ok := m["outcome"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Cancelled = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionOutcomeSelected
+ var match bool = true
+ if _, ok := m["outcome"]; !ok {
+ match = false
+ }
+ if _, ok := m["optionId"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Selected = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionOutcomeCancelled
+ if json.Unmarshal(b, &v) == nil {
+ u.Cancelled = &v
+ return nil
+ }
+ }
+ {
+ var v RequestPermissionOutcomeSelected
+ if json.Unmarshal(b, &v) == nil {
+ u.Selected = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u RequestPermissionOutcome) MarshalJSON() ([]byte, error) {
+ if u.Cancelled != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Cancelled)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["outcome"] = "cancelled"
+ return json.Marshal(m)
+ }
+ if u.Selected != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Selected)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["outcome"] = "selected"
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+func (u *RequestPermissionOutcome) Validate() error {
+ var count int
+ if u.Cancelled != nil {
+ count++
+ }
+ if u.Selected != nil {
+ count++
+ }
+ if count != 1 {
+ return errors.New("RequestPermissionOutcome must have exactly one variant set")
+ }
+ return nil
+}
+
+// Request for user permission to execute a tool call. Sent when the agent needs authorization before performing a sensitive operation. See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
+type RequestPermissionRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Available permission options for the user to choose from.
+ Options []PermissionOption `json:"options"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+ // Details about the tool call requiring permission.
+ ToolCall ToolCallUpdate `json:"toolCall"`
+}
+
+func (v *RequestPermissionRequest) Validate() error {
+ if v.Options == nil {
+ return fmt.Errorf("options is required")
+ }
+ return nil
+}
+
+// Response to a permission request.
+type RequestPermissionResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The user's decision on the permission request.
+ Outcome RequestPermissionOutcome `json:"outcome"`
+}
+
+func (v *RequestPermissionResponse) Validate() error {
+ return nil
+}
+
+// A resource that the server is capable of reading, included in a prompt or tool call result.
+type ResourceLink struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Description *string `json:"description,omitempty"`
+ MimeType *string `json:"mimeType,omitempty"`
+ Name string `json:"name"`
+ Size *int `json:"size,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Uri string `json:"uri"`
+}
+
+// The sender or recipient of messages and data in a conversation.
+type Role string
+
+const (
+ RoleAssistant Role = "assistant"
+ RoleUser Role = "user"
+)
+
+// A unique identifier for a conversation session between a client and agent. Sessions maintain their own context, conversation history, and state, allowing multiple independent interactions with the same agent. # Example ”' use agent_client_protocol::SessionId; use std::sync::Arc; let session_id = SessionId(Arc::from("sess_abc123def456")); ”' See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)
+type SessionId string
+
+// A mode the agent can operate in. See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes)
+type SessionMode struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Description *string `json:"description,omitempty"`
+ Id SessionModeId `json:"id"`
+ Name string `json:"name"`
+}
+
+// Unique identifier for a Session Mode.
+type SessionModeId string
+
+// The set of modes and the one currently active.
+type SessionModeState struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The set of modes that the Agent can operate in
+ AvailableModes []SessionMode `json:"availableModes"`
+ // The current mode the Agent is in.
+ CurrentModeId SessionModeId `json:"currentModeId"`
+}
+
+// **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. The set of models and the one currently active.
+type SessionModelState struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The set of models that the Agent can use
+ AvailableModels []ModelInfo `json:"availableModels"`
+ // The current model the Agent is in.
+ CurrentModelId ModelId `json:"currentModelId"`
+}
+
+// Notification containing a session update from the agent. Used to stream real-time progress and results during prompt processing. See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
+type SessionNotification struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The ID of the session this update pertains to.
+ SessionId SessionId `json:"sessionId"`
+ // The actual update content.
+ Update SessionUpdate `json:"update"`
+}
+
+func (v *SessionNotification) Validate() error {
+ return nil
+}
+
+// Different types of updates that can be sent during session processing. These updates provide real-time feedback about the agent's progress. See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
+// A chunk of the user's message being streamed.
+type SessionUpdateUserMessageChunk struct {
+ Content ContentBlock `json:"content"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+// A chunk of the agent's response being streamed.
+type SessionUpdateAgentMessageChunk struct {
+ Content ContentBlock `json:"content"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+// A chunk of the agent's internal reasoning being streamed.
+type SessionUpdateAgentThoughtChunk struct {
+ Content ContentBlock `json:"content"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+// Notification that a new tool call has been initiated.
+type SessionUpdateToolCall struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Content produced by the tool call.
+ Content []ToolCallContent `json:"content,omitempty"`
+ // The category of tool being invoked. Helps clients choose appropriate icons and UI treatment.
+ Kind ToolKind `json:"kind,omitempty"`
+ // File locations affected by this tool call. Enables "follow-along" features in clients.
+ Locations []ToolCallLocation `json:"locations,omitempty"`
+ // Raw input parameters sent to the tool.
+ RawInput any `json:"rawInput,omitempty"`
+ // Raw output returned by the tool.
+ RawOutput any `json:"rawOutput,omitempty"`
+ SessionUpdate string `json:"sessionUpdate"`
+ // Current execution status of the tool call.
+ Status ToolCallStatus `json:"status,omitempty"`
+ // Human-readable title describing what the tool is doing.
+ Title string `json:"title"`
+ // Unique identifier for this tool call within the session.
+ ToolCallId ToolCallId `json:"toolCallId"`
+}
+
+// Update on the status or results of a tool call.
+type SessionUpdateToolCallUpdate struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Replace the content collection.
+ Content []ToolCallContent `json:"content,omitempty"`
+ // Update the tool kind.
+ Kind *ToolKind `json:"kind,omitempty"`
+ // Replace the locations collection.
+ Locations []ToolCallLocation `json:"locations,omitempty"`
+ // Update the raw input.
+ RawInput any `json:"rawInput,omitempty"`
+ // Update the raw output.
+ RawOutput any `json:"rawOutput,omitempty"`
+ SessionUpdate string `json:"sessionUpdate"`
+ // Update the execution status.
+ Status *ToolCallStatus `json:"status,omitempty"`
+ // Update the human-readable title.
+ Title *string `json:"title,omitempty"`
+ // The ID of the tool call being updated.
+ ToolCallId ToolCallId `json:"toolCallId"`
+}
+
+// The agent's execution plan for complex tasks. See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)
+type SessionUpdatePlan struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The list of tasks to be accomplished. When updating a plan, the agent must send a complete list of all entries with their current status. The client replaces the entire plan with each update.
+ Entries []PlanEntry `json:"entries"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+// Available commands are ready or have changed
+type SessionUpdateAvailableCommandsUpdate struct {
+ AvailableCommands []AvailableCommand `json:"availableCommands"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+// The current mode of the session has changed See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes)
+type SessionUpdateCurrentModeUpdate struct {
+ CurrentModeId SessionModeId `json:"currentModeId"`
+ SessionUpdate string `json:"sessionUpdate"`
+}
+
+type SessionUpdate struct {
+ UserMessageChunk *SessionUpdateUserMessageChunk `json:"-"`
+ AgentMessageChunk *SessionUpdateAgentMessageChunk `json:"-"`
+ AgentThoughtChunk *SessionUpdateAgentThoughtChunk `json:"-"`
+ ToolCall *SessionUpdateToolCall `json:"-"`
+ ToolCallUpdate *SessionUpdateToolCallUpdate `json:"-"`
+ Plan *SessionUpdatePlan `json:"-"`
+ AvailableCommandsUpdate *SessionUpdateAvailableCommandsUpdate `json:"-"`
+ CurrentModeUpdate *SessionUpdateCurrentModeUpdate `json:"-"`
+}
+
+func (u *SessionUpdate) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var disc string
+ if v, ok := m["sessionUpdate"]; ok {
+ json.Unmarshal(v, &disc)
+ }
+ switch disc {
+ case "user_message_chunk":
+ var v SessionUpdateUserMessageChunk
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.UserMessageChunk = &v
+ return nil
+ case "agent_message_chunk":
+ var v SessionUpdateAgentMessageChunk
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AgentMessageChunk = &v
+ return nil
+ case "agent_thought_chunk":
+ var v SessionUpdateAgentThoughtChunk
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AgentThoughtChunk = &v
+ return nil
+ case "tool_call":
+ var v SessionUpdateToolCall
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ToolCall = &v
+ return nil
+ case "tool_call_update":
+ var v SessionUpdateToolCallUpdate
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ToolCallUpdate = &v
+ return nil
+ case "plan":
+ var v SessionUpdatePlan
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Plan = &v
+ return nil
+ case "available_commands_update":
+ var v SessionUpdateAvailableCommandsUpdate
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AvailableCommandsUpdate = &v
+ return nil
+ case "current_mode_update":
+ var v SessionUpdateCurrentModeUpdate
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.CurrentModeUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateUserMessageChunk
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["content"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.UserMessageChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAgentMessageChunk
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["content"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AgentMessageChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAgentThoughtChunk
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["content"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AgentThoughtChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateToolCall
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["toolCallId"]; !ok {
+ match = false
+ }
+ if _, ok := m["title"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ToolCall = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateToolCallUpdate
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["toolCallId"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.ToolCallUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdatePlan
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["entries"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Plan = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAvailableCommandsUpdate
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["availableCommands"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.AvailableCommandsUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateCurrentModeUpdate
+ var match bool = true
+ if _, ok := m["sessionUpdate"]; !ok {
+ match = false
+ }
+ if _, ok := m["currentModeId"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.CurrentModeUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateUserMessageChunk
+ if json.Unmarshal(b, &v) == nil {
+ u.UserMessageChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAgentMessageChunk
+ if json.Unmarshal(b, &v) == nil {
+ u.AgentMessageChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAgentThoughtChunk
+ if json.Unmarshal(b, &v) == nil {
+ u.AgentThoughtChunk = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateToolCall
+ if json.Unmarshal(b, &v) == nil {
+ u.ToolCall = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateToolCallUpdate
+ if json.Unmarshal(b, &v) == nil {
+ u.ToolCallUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdatePlan
+ if json.Unmarshal(b, &v) == nil {
+ u.Plan = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateAvailableCommandsUpdate
+ if json.Unmarshal(b, &v) == nil {
+ u.AvailableCommandsUpdate = &v
+ return nil
+ }
+ }
+ {
+ var v SessionUpdateCurrentModeUpdate
+ if json.Unmarshal(b, &v) == nil {
+ u.CurrentModeUpdate = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u SessionUpdate) MarshalJSON() ([]byte, error) {
+ if u.UserMessageChunk != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.UserMessageChunk)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "user_message_chunk"
+ return json.Marshal(m)
+ }
+ if u.AgentMessageChunk != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.AgentMessageChunk)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "agent_message_chunk"
+ return json.Marshal(m)
+ }
+ if u.AgentThoughtChunk != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.AgentThoughtChunk)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "agent_thought_chunk"
+ return json.Marshal(m)
+ }
+ if u.ToolCall != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ToolCall)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "tool_call"
+ return json.Marshal(m)
+ }
+ if u.ToolCallUpdate != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.ToolCallUpdate)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "tool_call_update"
+ return json.Marshal(m)
+ }
+ if u.Plan != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Plan)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "plan"
+ return json.Marshal(m)
+ }
+ if u.AvailableCommandsUpdate != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.AvailableCommandsUpdate)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "available_commands_update"
+ return json.Marshal(m)
+ }
+ if u.CurrentModeUpdate != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.CurrentModeUpdate)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["sessionUpdate"] = "current_mode_update"
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+func (u *SessionUpdate) Validate() error {
+ var count int
+ if u.UserMessageChunk != nil {
+ count++
+ }
+ if u.AgentMessageChunk != nil {
+ count++
+ }
+ if u.AgentThoughtChunk != nil {
+ count++
+ }
+ if u.ToolCall != nil {
+ count++
+ }
+ if u.ToolCallUpdate != nil {
+ count++
+ }
+ if u.Plan != nil {
+ count++
+ }
+ if u.AvailableCommandsUpdate != nil {
+ count++
+ }
+ if u.CurrentModeUpdate != nil {
+ count++
+ }
+ if count != 1 {
+ return errors.New("SessionUpdate must have exactly one variant set")
+ }
+ return nil
+}
+
+// Request parameters for setting a session mode.
+type SetSessionModeRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The ID of the mode to set.
+ ModeId SessionModeId `json:"modeId"`
+ // The ID of the session to set the mode for.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *SetSessionModeRequest) Validate() error {
+ return nil
+}
+
+// Response to 'session/set_mode' method.
+type SetSessionModeResponse struct {
+ Meta any `json:"meta,omitempty"`
+}
+
+func (v *SetSessionModeResponse) Validate() error {
+ return nil
+}
+
+// **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. Request parameters for setting a session model.
+type SetSessionModelRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The ID of the model to set.
+ ModelId ModelId `json:"modelId"`
+ // The ID of the session to set the model for.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *SetSessionModelRequest) Validate() error {
+ return nil
+}
+
+// **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. Response to 'session/set_model' method.
+type SetSessionModelResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+}
+
+func (v *SetSessionModelResponse) Validate() error {
+ return nil
+}
+
+// Reasons why an agent stops processing a prompt turn. See protocol docs: [Stop Reasons](https://agentclientprotocol.com/protocol/prompt-turn#stop-reasons)
+type StopReason string
+
+const (
+ StopReasonEndTurn StopReason = "end_turn"
+ StopReasonMaxTokens StopReason = "max_tokens"
+ StopReasonMaxTurnRequests StopReason = "max_turn_requests"
+ StopReasonRefusal StopReason = "refusal"
+ StopReasonCancelled StopReason = "cancelled"
+)
+
+// Exit status of a terminal command.
+type TerminalExitStatus struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The process exit code (may be null if terminated by signal).
+ ExitCode *int `json:"exitCode,omitempty"`
+ // The signal that terminated the process (may be null if exited normally).
+ Signal *string `json:"signal,omitempty"`
+}
+
+// Request to get the current output and status of a terminal.
+type TerminalOutputRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+ // The ID of the terminal to get output from.
+ TerminalId string `json:"terminalId"`
+}
+
+func (v *TerminalOutputRequest) Validate() error {
+ if v.TerminalId == "" {
+ return fmt.Errorf("terminalId is required")
+ }
+ return nil
+}
+
+// Response containing the terminal output and exit status.
+type TerminalOutputResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Exit status if the command has completed.
+ ExitStatus *TerminalExitStatus `json:"exitStatus,omitempty"`
+ // The terminal output captured so far.
+ Output string `json:"output"`
+ // Whether the output was truncated due to byte limits.
+ Truncated bool `json:"truncated"`
+}
+
+func (v *TerminalOutputResponse) Validate() error {
+ if v.Output == "" {
+ return fmt.Errorf("output is required")
+ }
+ return nil
+}
+
+// Text provided to or from an LLM.
+type TextContent struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ Annotations *Annotations `json:"annotations,omitempty"`
+ Text string `json:"text"`
+}
+
+// Text-based resource contents.
+type TextResourceContents struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ MimeType *string `json:"mimeType,omitempty"`
+ Text string `json:"text"`
+ Uri string `json:"uri"`
+}
+
+// Represents a tool call that the language model has requested. Tool calls are actions that the agent executes on behalf of the language model, such as reading files, executing code, or fetching data from external sources. See protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-calls)
+type ToolCall struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Content produced by the tool call.
+ Content []ToolCallContent `json:"content,omitempty"`
+ // The category of tool being invoked. Helps clients choose appropriate icons and UI treatment.
+ Kind ToolKind `json:"kind,omitempty"`
+ // File locations affected by this tool call. Enables "follow-along" features in clients.
+ Locations []ToolCallLocation `json:"locations,omitempty"`
+ // Raw input parameters sent to the tool.
+ RawInput any `json:"rawInput,omitempty"`
+ // Raw output returned by the tool.
+ RawOutput any `json:"rawOutput,omitempty"`
+ // Current execution status of the tool call.
+ Status ToolCallStatus `json:"status,omitempty"`
+ // Human-readable title describing what the tool is doing.
+ Title string `json:"title"`
+ // Unique identifier for this tool call within the session.
+ ToolCallId ToolCallId `json:"toolCallId"`
+}
+
+// Content produced by a tool call. Tool calls can produce different types of content including standard content blocks (text, images) or file diffs. See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
+// Standard content block (text, images, resources).
+type ToolCallContentContent struct {
+ // The actual content block.
+ Content ContentBlock `json:"content"`
+ Type string `json:"type"`
+}
+
+// File modification shown as a diff.
+type ToolCallContentDiff struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The new content after modification.
+ NewText string `json:"newText"`
+ // The original content (None for new files).
+ OldText *string `json:"oldText,omitempty"`
+ // The file path being modified.
+ Path string `json:"path"`
+ Type string `json:"type"`
+}
+
+// Embed a terminal created with 'terminal/create' by its id. The terminal must be added before calling 'terminal/release'. See protocol docs: [Terminal](https://agentclientprotocol.com/protocol/terminal)
+type ToolCallContentTerminal struct {
+ TerminalId string `json:"terminalId"`
+ Type string `json:"type"`
+}
+
+type ToolCallContent struct {
+ Content *ToolCallContentContent `json:"-"`
+ Diff *ToolCallContentDiff `json:"-"`
+ Terminal *ToolCallContentTerminal `json:"-"`
+}
+
+func (u *ToolCallContent) UnmarshalJSON(b []byte) error {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+ {
+ var disc string
+ if v, ok := m["type"]; ok {
+ json.Unmarshal(v, &disc)
+ }
+ switch disc {
+ case "content":
+ var v ToolCallContentContent
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Content = &v
+ return nil
+ case "diff":
+ var v ToolCallContentDiff
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Diff = &v
+ return nil
+ case "terminal":
+ var v ToolCallContentTerminal
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Terminal = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentContent
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["content"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Content = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentDiff
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["path"]; !ok {
+ match = false
+ }
+ if _, ok := m["newText"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Diff = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentTerminal
+ var match bool = true
+ if _, ok := m["type"]; !ok {
+ match = false
+ }
+ if _, ok := m["terminalId"]; !ok {
+ match = false
+ }
+ if match {
+ if json.Unmarshal(b, &v) != nil {
+ return errors.New("invalid variant payload")
+ }
+ u.Terminal = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentContent
+ if json.Unmarshal(b, &v) == nil {
+ u.Content = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentDiff
+ if json.Unmarshal(b, &v) == nil {
+ u.Diff = &v
+ return nil
+ }
+ }
+ {
+ var v ToolCallContentTerminal
+ if json.Unmarshal(b, &v) == nil {
+ u.Terminal = &v
+ return nil
+ }
+ }
+ return nil
+}
+func (u ToolCallContent) MarshalJSON() ([]byte, error) {
+ if u.Content != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Content)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "content"
+ return json.Marshal(m)
+ }
+ if u.Diff != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Diff)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "diff"
+ return json.Marshal(m)
+ }
+ if u.Terminal != nil {
+ var m map[string]any
+ _b, _e := json.Marshal(*u.Terminal)
+ if _e != nil {
+ return []byte{}, _e
+ }
+ if json.Unmarshal(_b, &m) != nil {
+ return []byte{}, errors.New("invalid variant payload")
+ }
+ m["type"] = "terminal"
+ return json.Marshal(m)
+ }
+ return []byte{}, nil
+}
+
+func (u *ToolCallContent) Validate() error {
+ var count int
+ if u.Content != nil {
+ count++
+ }
+ if u.Diff != nil {
+ count++
+ }
+ if u.Terminal != nil {
+ count++
+ }
+ if count != 1 {
+ return errors.New("ToolCallContent must have exactly one variant set")
+ }
+ return nil
+}
+
+// Unique identifier for a tool call within a session.
+type ToolCallId string
+
+// A file location being accessed or modified by a tool. Enables clients to implement "follow-along" features that track which files the agent is working with in real-time. See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
+type ToolCallLocation struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Optional line number within the file.
+ Line *int `json:"line,omitempty"`
+ // The file path being accessed or modified.
+ Path string `json:"path"`
+}
+
+// Execution status of a tool call. Tool calls progress through different statuses during their lifecycle. See protocol docs: [Status](https://agentclientprotocol.com/protocol/tool-calls#status)
+type ToolCallStatus string
+
+const (
+ ToolCallStatusPending ToolCallStatus = "pending"
+ ToolCallStatusInProgress ToolCallStatus = "in_progress"
+ ToolCallStatusCompleted ToolCallStatus = "completed"
+ ToolCallStatusFailed ToolCallStatus = "failed"
+)
+
+// An update to an existing tool call. Used to report progress and results as tools execute. All fields except the tool call ID are optional - only changed fields need to be included. See protocol docs: [Updating](https://agentclientprotocol.com/protocol/tool-calls#updating)
+type ToolCallUpdate struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // Replace the content collection.
+ Content []ToolCallContent `json:"content,omitempty"`
+ // Update the tool kind.
+ Kind *ToolKind `json:"kind,omitempty"`
+ // Replace the locations collection.
+ Locations []ToolCallLocation `json:"locations,omitempty"`
+ // Update the raw input.
+ RawInput any `json:"rawInput,omitempty"`
+ // Update the raw output.
+ RawOutput any `json:"rawOutput,omitempty"`
+ // Update the execution status.
+ Status *ToolCallStatus `json:"status,omitempty"`
+ // Update the human-readable title.
+ Title *string `json:"title,omitempty"`
+ // The ID of the tool call being updated.
+ ToolCallId ToolCallId `json:"toolCallId"`
+}
+
+func (t *ToolCallUpdate) Validate() error {
+ if t.ToolCallId == "" {
+ return fmt.Errorf("toolCallId is required")
+ }
+ return nil
+}
+
+// Categories of tools that can be invoked. Tool kinds help clients choose appropriate icons and optimize how they display tool execution progress. See protocol docs: [Creating](https://agentclientprotocol.com/protocol/tool-calls#creating)
+type ToolKind string
+
+const (
+ ToolKindRead ToolKind = "read"
+ ToolKindEdit ToolKind = "edit"
+ ToolKindDelete ToolKind = "delete"
+ ToolKindMove ToolKind = "move"
+ ToolKindSearch ToolKind = "search"
+ ToolKindExecute ToolKind = "execute"
+ ToolKindThink ToolKind = "think"
+ ToolKindFetch ToolKind = "fetch"
+ ToolKindSwitchMode ToolKind = "switch_mode"
+ ToolKindOther ToolKind = "other"
+)
+
+// Request to wait for a terminal command to exit.
+type WaitForTerminalExitRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+ // The ID of the terminal to wait for.
+ TerminalId string `json:"terminalId"`
+}
+
+func (v *WaitForTerminalExitRequest) Validate() error {
+ if v.TerminalId == "" {
+ return fmt.Errorf("terminalId is required")
+ }
+ return nil
+}
+
+// Response containing the exit status of a terminal command.
+type WaitForTerminalExitResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The process exit code (may be null if terminated by signal).
+ ExitCode *int `json:"exitCode,omitempty"`
+ // The signal that terminated the process (may be null if exited normally).
+ Signal *string `json:"signal,omitempty"`
+}
+
+func (v *WaitForTerminalExitResponse) Validate() error {
+ return nil
+}
+
+// Request to write content to a text file. Only available if the client supports the 'fs.writeTextFile' capability.
+type WriteTextFileRequest struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+ // The text content to write to the file.
+ Content string `json:"content"`
+ // Absolute path to the file to write.
+ Path string `json:"path"`
+ // The session ID for this request.
+ SessionId SessionId `json:"sessionId"`
+}
+
+func (v *WriteTextFileRequest) Validate() error {
+ if v.Content == "" {
+ return fmt.Errorf("content is required")
+ }
+ if v.Path == "" {
+ return fmt.Errorf("path is required")
+ }
+ return nil
+}
+
+// Response to 'fs/write_text_file'
+type WriteTextFileResponse struct {
+ // Extension point for implementations
+ Meta any `json:"_meta,omitempty"`
+}
+
+func (v *WriteTextFileResponse) Validate() error {
+ return nil
+}
+
+type Agent interface {
+ Authenticate(ctx context.Context, params AuthenticateRequest) (AuthenticateResponse, error)
+ Initialize(ctx context.Context, params InitializeRequest) (InitializeResponse, error)
+ Cancel(ctx context.Context, params CancelNotification) error
+ NewSession(ctx context.Context, params NewSessionRequest) (NewSessionResponse, error)
+ Prompt(ctx context.Context, params PromptRequest) (PromptResponse, error)
+ SetSessionMode(ctx context.Context, params SetSessionModeRequest) (SetSessionModeResponse, error)
+}
+
+// AgentLoader defines optional support for loading sessions. Implement and advertise the capability to enable 'session/load'.
+type AgentLoader interface {
+ LoadSession(ctx context.Context, params LoadSessionRequest) (LoadSessionResponse, error)
+}
+
+// AgentExperimental defines unstable methods that are not part of the official spec. These may change or be removed without notice.
+type AgentExperimental interface {
+ SetSessionModel(ctx context.Context, params SetSessionModelRequest) (SetSessionModelResponse, error)
+}
+type Client interface {
+ ReadTextFile(ctx context.Context, params ReadTextFileRequest) (ReadTextFileResponse, error)
+ WriteTextFile(ctx context.Context, params WriteTextFileRequest) (WriteTextFileResponse, error)
+ RequestPermission(ctx context.Context, params RequestPermissionRequest) (RequestPermissionResponse, error)
+ SessionUpdate(ctx context.Context, params SessionNotification) error
+ CreateTerminal(ctx context.Context, params CreateTerminalRequest) (CreateTerminalResponse, error)
+ KillTerminalCommand(ctx context.Context, params KillTerminalCommandRequest) (KillTerminalCommandResponse, error)
+ TerminalOutput(ctx context.Context, params TerminalOutputRequest) (TerminalOutputResponse, error)
+ ReleaseTerminal(ctx context.Context, params ReleaseTerminalRequest) (ReleaseTerminalResponse, error)
+ WaitForTerminalExit(ctx context.Context, params WaitForTerminalExitRequest) (WaitForTerminalExitResponse, error)
+}
+
+// ClientExperimental defines unstable methods that are not part of the official spec. These may change or be removed without notice.
+type ClientExperimental interface{}
diff --git a/package.json b/package.json
index e9882690..d86f18ed 100644
--- a/package.json
+++ b/package.json
@@ -30,21 +30,25 @@
"scripts": {
"prepublishOnly": "cp typescript/README.md README.md",
"postpublish": "git checkout README.md",
- "clean": "rm -rf dist tsconfig.tsbuildinfo && cargo clean",
- "test": "cargo check --all-targets && cargo test && vitest run",
+ "clean": "rm -rf dist typescript/*.js typescript/*.d.ts typescript/*.js.map tsconfig.tsbuildinfo && cargo clean && (cd go && go clean -cache -testcache)",
+ "test": "cargo check --all-targets && cargo test && vitest run && npm run test:go",
+ "test:go": "cd go && go test ./...",
+ "test:go:race": "cd go && go test -race ./...",
"test:ts": "vitest run",
"test:ts:watch": "vitest",
"generate:json-schema": "cd rust && cargo run --bin generate --features unstable",
"generate:ts-schema": "node typescript/generate.js",
- "generate": "npm run generate:json-schema && npm run generate:ts-schema && npm run format",
+ "generate:go": "cd go/cmd/generate && env -u GOPATH -u GOMODCACHE go run . && cd ../.. && env -u GOPATH -u GOMODCACHE go run mvdan.cc/gofumpt@latest -w .",
+ "generate": "npm run generate:json-schema && npm run generate:ts-schema && npm run generate:go && npm run format",
"build": "npm run generate && tsc",
- "format": "prettier --write . && cargo fmt",
- "format:check": "prettier --check . && cargo fmt -- --check",
- "lint": "cargo clippy",
+ "format": "prettier --write . && cargo fmt && (cd go && env -u GOPATH -u GOMODCACHE go run mvdan.cc/gofumpt@latest -w .)",
+ "format:check": "prettier --check . && cargo fmt -- --check && (cd go && test -z \"$(env -u GOPATH -u GOMODCACHE go run mvdan.cc/gofumpt@latest -l .)\" || (echo 'gofumpt: found unformatted Go files' >&2; exit 1))",
+ "lint": "cargo clippy && (cd go && go vet ./...)",
"lint:fix": "cargo clippy --fix",
+ "check:go": "cd go && go build ./...",
"spellcheck": "./scripts/spellcheck.sh",
"spellcheck:fix": "./scripts/spellcheck.sh --write-changes",
- "check": "npm run lint && npm run format:check && npm run spellcheck && npm run build && npm run test && npm run docs:ts:verify",
+ "check": "npm run lint && npm run format:check && npm run spellcheck && npm run build && npm run test && npm run docs:ts:verify && npm run check:go",
"docs": "cd docs && npx mint@4.2.93 dev",
"docs:ts:build": "cd typescript && typedoc && echo 'TypeScript documentation generated in ./typescript/docs'",
"docs:ts:dev": "concurrently \"cd typescript && typedoc --watch --preserveWatchOutput\" \"npx http-server typescript/docs -p 8081\"",