|
| 1 | +// Package acp implements the Agent Client Protocol (ACP) for GPTCode CLI. |
| 2 | +// |
| 3 | +// ACP is a JSON-RPC 2.0 based protocol that standardizes communication between |
| 4 | +// code editors/IDEs and AI coding agents. It is analogous to LSP but for AI agents. |
| 5 | +// |
| 6 | +// Transport: stdio (newline-delimited JSON-RPC messages) |
| 7 | +// Spec: https://agentclientprotocol.com |
| 8 | +package acp |
| 9 | + |
| 10 | +import ( |
| 11 | + "encoding/json" |
| 12 | + "fmt" |
| 13 | +) |
| 14 | + |
| 15 | +// ProtocolVersion is the ACP protocol version we support. |
| 16 | +const ProtocolVersion = 1 |
| 17 | + |
| 18 | +// --- JSON-RPC 2.0 types --- |
| 19 | + |
| 20 | +// Request represents a JSON-RPC 2.0 request from client or agent. |
| 21 | +type Request struct { |
| 22 | + JSONRPC string `json:"jsonrpc"` |
| 23 | + ID interface{} `json:"id"` |
| 24 | + Method string `json:"method"` |
| 25 | + Params json.RawMessage `json:"params,omitempty"` |
| 26 | +} |
| 27 | + |
| 28 | +// Response represents a JSON-RPC 2.0 response. |
| 29 | +type Response struct { |
| 30 | + JSONRPC string `json:"jsonrpc"` |
| 31 | + ID interface{} `json:"id"` |
| 32 | + Result interface{} `json:"result,omitempty"` |
| 33 | + Error *RPCError `json:"error,omitempty"` |
| 34 | +} |
| 35 | + |
| 36 | +// Notification represents a JSON-RPC 2.0 notification (no ID, no response expected). |
| 37 | +type Notification struct { |
| 38 | + JSONRPC string `json:"jsonrpc"` |
| 39 | + Method string `json:"method"` |
| 40 | + Params interface{} `json:"params,omitempty"` |
| 41 | +} |
| 42 | + |
| 43 | +// RPCError represents a JSON-RPC 2.0 error object. |
| 44 | +type RPCError struct { |
| 45 | + Code int `json:"code"` |
| 46 | + Message string `json:"message"` |
| 47 | + Data interface{} `json:"data,omitempty"` |
| 48 | +} |
| 49 | + |
| 50 | +func (e *RPCError) Error() string { |
| 51 | + return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message) |
| 52 | +} |
| 53 | + |
| 54 | +// Standard JSON-RPC 2.0 error codes. |
| 55 | +const ( |
| 56 | + CodeParseError = -32700 |
| 57 | + CodeInvalidRequest = -32600 |
| 58 | + CodeMethodNotFound = -32601 |
| 59 | + CodeInvalidParams = -32602 |
| 60 | + CodeInternalError = -32603 |
| 61 | +) |
| 62 | + |
| 63 | +// --- ACP Method constants --- |
| 64 | + |
| 65 | +const ( |
| 66 | + // Agent baseline methods (client → agent) |
| 67 | + MethodInitialize = "initialize" |
| 68 | + MethodAuthenticate = "authenticate" |
| 69 | + MethodSessionNew = "session/new" |
| 70 | + MethodSessionPrompt = "session/prompt" |
| 71 | + |
| 72 | + // Agent optional methods |
| 73 | + MethodSessionLoad = "session/load" |
| 74 | + MethodSessionSetMode = "session/set_mode" |
| 75 | + |
| 76 | + // Agent notifications (client → agent) |
| 77 | + MethodSessionCancel = "session/cancel" |
| 78 | + |
| 79 | + // Client baseline methods (agent → client) |
| 80 | + MethodRequestPermission = "session/request_permission" |
| 81 | + |
| 82 | + // Client optional methods (agent → client) |
| 83 | + MethodFSReadTextFile = "fs/read_text_file" |
| 84 | + MethodFSWriteTextFile = "fs/write_text_file" |
| 85 | + MethodTerminalCreate = "terminal/create" |
| 86 | + MethodTerminalOutput = "terminal/output" |
| 87 | + MethodTerminalRelease = "terminal/release" |
| 88 | + MethodTerminalWaitExit = "terminal/wait_for_exit" |
| 89 | + MethodTerminalKill = "terminal/kill" |
| 90 | + |
| 91 | + // Client notifications (agent → client) |
| 92 | + MethodSessionUpdate = "session/update" |
| 93 | +) |
| 94 | + |
| 95 | +// --- Initialization types --- |
| 96 | + |
| 97 | +// InitializeParams is sent by the client in the initialize request. |
| 98 | +type InitializeParams struct { |
| 99 | + ProtocolVersion int `json:"protocolVersion"` |
| 100 | + ClientCapabilities ClientCapabilities `json:"clientCapabilities"` |
| 101 | + ClientInfo ImplementationInfo `json:"clientInfo"` |
| 102 | +} |
| 103 | + |
| 104 | +// InitializeResult is the agent's response to initialize. |
| 105 | +type InitializeResult struct { |
| 106 | + ProtocolVersion int `json:"protocolVersion"` |
| 107 | + AgentCapabilities AgentCapabilities `json:"agentCapabilities"` |
| 108 | + AgentInfo ImplementationInfo `json:"agentInfo"` |
| 109 | + AuthMethods []interface{} `json:"authMethods"` |
| 110 | +} |
| 111 | + |
| 112 | +// ClientCapabilities describes what the client can do. |
| 113 | +type ClientCapabilities struct { |
| 114 | + FS *FSCapabilities `json:"fs,omitempty"` |
| 115 | + Terminal bool `json:"terminal,omitempty"` |
| 116 | +} |
| 117 | + |
| 118 | +// FSCapabilities describes file system capabilities of the client. |
| 119 | +type FSCapabilities struct { |
| 120 | + ReadTextFile bool `json:"readTextFile,omitempty"` |
| 121 | + WriteTextFile bool `json:"writeTextFile,omitempty"` |
| 122 | +} |
| 123 | + |
| 124 | +// AgentCapabilities describes what the agent supports. |
| 125 | +type AgentCapabilities struct { |
| 126 | + LoadSession bool `json:"loadSession"` |
| 127 | + PromptCapabilities PromptCapabilities `json:"promptCapabilities"` |
| 128 | + MCPCapabilities MCPCapabilities `json:"mcpCapabilities"` |
| 129 | +} |
| 130 | + |
| 131 | +// PromptCapabilities describes what content types the agent accepts. |
| 132 | +type PromptCapabilities struct { |
| 133 | + Image bool `json:"image"` |
| 134 | + Audio bool `json:"audio"` |
| 135 | + EmbeddedContext bool `json:"embeddedContext"` |
| 136 | +} |
| 137 | + |
| 138 | +// MCPCapabilities describes MCP support. |
| 139 | +type MCPCapabilities struct { |
| 140 | + HTTP bool `json:"http"` |
| 141 | + SSE bool `json:"sse"` |
| 142 | +} |
| 143 | + |
| 144 | +// ImplementationInfo identifies an agent or client implementation. |
| 145 | +type ImplementationInfo struct { |
| 146 | + Name string `json:"name"` |
| 147 | + Title string `json:"title"` |
| 148 | + Version string `json:"version"` |
| 149 | +} |
| 150 | + |
| 151 | +// --- Session types --- |
| 152 | + |
| 153 | +// SessionNewParams is the params for session/new. |
| 154 | +type SessionNewParams struct { |
| 155 | + WorkingDirectory string `json:"workingDirectory,omitempty"` |
| 156 | + ConfigOptions map[string]string `json:"configOptions,omitempty"` |
| 157 | +} |
| 158 | + |
| 159 | +// SessionNewResult is the result of session/new. |
| 160 | +type SessionNewResult struct { |
| 161 | + SessionID string `json:"sessionId"` |
| 162 | +} |
| 163 | + |
| 164 | +// SessionPromptParams is the params for session/prompt. |
| 165 | +type SessionPromptParams struct { |
| 166 | + SessionID string `json:"sessionId"` |
| 167 | + Content []ContentBlock `json:"content"` |
| 168 | +} |
| 169 | + |
| 170 | +// ContentBlock represents a piece of content in a prompt or update. |
| 171 | +type ContentBlock struct { |
| 172 | + Type string `json:"type"` // "text", "image", "resource", "resourceLink" |
| 173 | + Text string `json:"text,omitempty"` |
| 174 | +} |
| 175 | + |
| 176 | +// SessionPromptResult is the result of session/prompt (returned when the turn ends). |
| 177 | +type SessionPromptResult struct { |
| 178 | + StopReason string `json:"stopReason"` // "endTurn", "cancelled", "maxTurns" |
| 179 | +} |
| 180 | + |
| 181 | +// --- Update notification types --- |
| 182 | + |
| 183 | +// SessionUpdateParams is sent as a notification from agent to client during a turn. |
| 184 | +type SessionUpdateParams struct { |
| 185 | + SessionID string `json:"sessionId"` |
| 186 | + Update SessionUpdate `json:"update"` |
| 187 | +} |
| 188 | + |
| 189 | +// SessionUpdate is the polymorphic update payload. |
| 190 | +type SessionUpdate struct { |
| 191 | + Kind string `json:"kind"` // "message", "toolCall", "toolCallUpdate", "plan", "availableCommands", "modeChange" |
| 192 | + |
| 193 | + // For kind="message" |
| 194 | + MessageChunk *MessageChunk `json:"messageChunk,omitempty"` |
| 195 | + |
| 196 | + // For kind="toolCall" |
| 197 | + ToolCall *ToolCallUpdate `json:"toolCall,omitempty"` |
| 198 | + |
| 199 | + // For kind="toolCallUpdate" |
| 200 | + ToolCallDelta *ToolCallDelta `json:"toolCallDelta,omitempty"` |
| 201 | + |
| 202 | + // For kind="plan" |
| 203 | + Plan *PlanUpdate `json:"plan,omitempty"` |
| 204 | + |
| 205 | + // For kind="availableCommands" |
| 206 | + Commands []SlashCommand `json:"commands,omitempty"` |
| 207 | +} |
| 208 | + |
| 209 | +// MessageChunk is a text chunk streamed to the client. |
| 210 | +type MessageChunk struct { |
| 211 | + Content []ContentBlock `json:"content"` |
| 212 | +} |
| 213 | + |
| 214 | +// ToolCallUpdate notifies the client about a tool invocation. |
| 215 | +type ToolCallUpdate struct { |
| 216 | + ToolCallID string `json:"toolCallId"` |
| 217 | + ToolName string `json:"toolName"` |
| 218 | + Status string `json:"status"` // "running", "completed", "error" |
| 219 | + Input string `json:"input,omitempty"` |
| 220 | + Output string `json:"output,omitempty"` |
| 221 | +} |
| 222 | + |
| 223 | +// ToolCallDelta is a partial update for a long-running tool call. |
| 224 | +type ToolCallDelta struct { |
| 225 | + ToolCallID string `json:"toolCallId"` |
| 226 | + Output string `json:"output,omitempty"` |
| 227 | +} |
| 228 | + |
| 229 | +// PlanUpdate provides plan information to the client. |
| 230 | +type PlanUpdate struct { |
| 231 | + Title string `json:"title"` |
| 232 | + Steps []PlanStep `json:"steps"` |
| 233 | +} |
| 234 | + |
| 235 | +// PlanStep is a single step in an agent plan. |
| 236 | +type PlanStep struct { |
| 237 | + Title string `json:"title"` |
| 238 | + Status string `json:"status"` // "pending", "running", "completed", "error" |
| 239 | +} |
| 240 | + |
| 241 | +// SlashCommand describes an available slash command. |
| 242 | +type SlashCommand struct { |
| 243 | + Name string `json:"name"` |
| 244 | + Description string `json:"description"` |
| 245 | +} |
| 246 | + |
| 247 | +// --- Permission types --- |
| 248 | + |
| 249 | +// RequestPermissionParams is sent from agent to client. |
| 250 | +type RequestPermissionParams struct { |
| 251 | + SessionID string `json:"sessionId"` |
| 252 | + Permissions []Permission `json:"permissions"` |
| 253 | +} |
| 254 | + |
| 255 | +// Permission is a single permission request. |
| 256 | +type Permission struct { |
| 257 | + Type string `json:"type"` // "fileWrite", "command" |
| 258 | + Description string `json:"description"` |
| 259 | + FilePath string `json:"filePath,omitempty"` |
| 260 | + Command string `json:"command,omitempty"` |
| 261 | +} |
| 262 | + |
| 263 | +// RequestPermissionResult is the client's response. |
| 264 | +type RequestPermissionResult struct { |
| 265 | + Granted bool `json:"granted"` |
| 266 | +} |
| 267 | + |
| 268 | +// --- File system types --- |
| 269 | + |
| 270 | +// FSReadTextFileParams for fs/read_text_file request. |
| 271 | +type FSReadTextFileParams struct { |
| 272 | + Path string `json:"path"` |
| 273 | +} |
| 274 | + |
| 275 | +// FSReadTextFileResult for fs/read_text_file response. |
| 276 | +type FSReadTextFileResult struct { |
| 277 | + Content string `json:"content"` |
| 278 | +} |
| 279 | + |
| 280 | +// FSWriteTextFileParams for fs/write_text_file request. |
| 281 | +type FSWriteTextFileParams struct { |
| 282 | + Path string `json:"path"` |
| 283 | + Content string `json:"content"` |
| 284 | +} |
| 285 | + |
| 286 | +// FSWriteTextFileResult for fs/write_text_file response. |
| 287 | +type FSWriteTextFileResult struct { |
| 288 | + Success bool `json:"success"` |
| 289 | +} |
| 290 | + |
| 291 | +// --- Terminal types --- |
| 292 | + |
| 293 | +// TerminalCreateParams for terminal/create request. |
| 294 | +type TerminalCreateParams struct { |
| 295 | + Command string `json:"command"` |
| 296 | + Cwd string `json:"cwd,omitempty"` |
| 297 | +} |
| 298 | + |
| 299 | +// TerminalCreateResult for terminal/create response. |
| 300 | +type TerminalCreateResult struct { |
| 301 | + TerminalID string `json:"terminalId"` |
| 302 | +} |
| 303 | + |
| 304 | +// TerminalWaitExitResult for terminal/wait_for_exit response. |
| 305 | +type TerminalWaitExitResult struct { |
| 306 | + ExitCode int `json:"exitCode"` |
| 307 | +} |
| 308 | + |
| 309 | +// --- Helpers --- |
| 310 | + |
| 311 | +// NewResponse creates a successful JSON-RPC response. |
| 312 | +func NewResponse(id interface{}, result interface{}) *Response { |
| 313 | + return &Response{ |
| 314 | + JSONRPC: "2.0", |
| 315 | + ID: id, |
| 316 | + Result: result, |
| 317 | + } |
| 318 | +} |
| 319 | + |
| 320 | +// NewErrorResponse creates an error JSON-RPC response. |
| 321 | +func NewErrorResponse(id interface{}, code int, message string) *Response { |
| 322 | + return &Response{ |
| 323 | + JSONRPC: "2.0", |
| 324 | + ID: id, |
| 325 | + Error: &RPCError{ |
| 326 | + Code: code, |
| 327 | + Message: message, |
| 328 | + }, |
| 329 | + } |
| 330 | +} |
| 331 | + |
| 332 | +// NewNotification creates a JSON-RPC notification. |
| 333 | +func NewNotification(method string, params interface{}) *Notification { |
| 334 | + return &Notification{ |
| 335 | + JSONRPC: "2.0", |
| 336 | + Method: method, |
| 337 | + Params: params, |
| 338 | + } |
| 339 | +} |
0 commit comments