| 🛟 Updated. 28d5985 k33g 15h ago | 1 | package acp |
| 2 | |
| 3 | import "encoding/json" |
| 4 | |
| 5 | // ProtocolVersion is the version of the Agent Client Protocol this client |
| 6 | // speaks. It is a single integer, and both sides have to agree on one. |
| 7 | const ProtocolVersion = 1 |
| 8 | |
| 9 | // The methods this client calls on an agent. |
| 10 | const ( |
| 11 | MethodInitialize = "initialize" |
| 12 | MethodNewSession = "session/new" |
| 13 | MethodPrompt = "session/prompt" |
| 14 | MethodCancel = "session/cancel" |
| 15 | ) |
| 16 | |
| 17 | // The methods an agent calls on this client. |
| 18 | const ( |
| 19 | MethodUpdate = "session/update" |
| 20 | MethodRequestPermission = "session/request_permission" |
| 21 | MethodReadTextFile = "fs/read_text_file" |
| 22 | MethodWriteTextFile = "fs/write_text_file" |
| 23 | ) |
| 24 | |
| 25 | // Implementation is what one side of the conversation calls itself. An agent |
| 26 | // logs it, so it is worth being truthful in. |
| 27 | type Implementation struct { |
| 28 | Name string `json:"name"` |
| 29 | Title string `json:"title,omitempty"` |
| 30 | Version string `json:"version,omitempty"` |
| 31 | } |
| 32 | |
| 33 | // FileSystemCapability says which file operations the editor will perform on |
| 34 | // the agent's behalf. |
| 35 | type FileSystemCapability struct { |
| 36 | ReadTextFile bool `json:"readTextFile"` |
| 37 | WriteTextFile bool `json:"writeTextFile"` |
| 38 | } |
| 39 | |
| 40 | // ClientCapabilities is what this editor offers an agent. |
| 41 | // |
| 42 | // Terminal is deliberately absent rather than false-and-present: an agent can |
| 43 | // already have a shell through its own toolsets, and advertising this one would |
| 44 | // mean the editor running commands on the agent's behalf and owning the output. |
| 45 | type ClientCapabilities struct { |
| 46 | FS FileSystemCapability `json:"fs"` |
| 47 | } |
| 48 | |
| 49 | // InitializeParams opens the conversation. |
| 50 | type InitializeParams struct { |
| 51 | ProtocolVersion int `json:"protocolVersion"` |
| 52 | ClientCapabilities ClientCapabilities `json:"clientCapabilities"` |
| 53 | ClientInfo Implementation `json:"clientInfo"` |
| 54 | } |
| 55 | |
| 56 | // AuthMethod is one way an agent offers to be logged in to. |
| 57 | type AuthMethod struct { |
| 58 | ID string `json:"id"` |
| 59 | Name string `json:"name"` |
| 60 | Description string `json:"description,omitempty"` |
| 61 | } |
| 62 | |
| 63 | // InitializeResult is what the agent answers with. |
| 64 | // |
| 65 | // AgentCapabilities is kept encoded: the set grows with the protocol, this |
| 66 | // client acts on one corner of it, and decoding the whole into a struct would |
| 67 | // turn every new capability into a field nobody reads. The corner it does act |
| 68 | // on is read out by PromptCapabilities. |
| 69 | type InitializeResult struct { |
| 70 | ProtocolVersion int `json:"protocolVersion"` |
| 71 | AgentCapabilities json.RawMessage `json:"agentCapabilities,omitempty"` |
| 72 | AgentInfo Implementation `json:"agentInfo"` |
| 73 | AuthMethods []AuthMethod `json:"authMethods,omitempty"` |
| 74 | } |
| 75 | |
| 76 | // PromptCapabilities is what an agent accepts inside a prompt beyond plain |
| 77 | // text. EmbeddedContext is the one that matters here: it says a file named |
| 78 | // with "@" may be sent with its text inside the prompt rather than as a link |
| 79 | // the agent has to follow. |
| 80 | type PromptCapabilities struct { |
| 81 | EmbeddedContext bool `json:"embeddedContext"` |
| 82 | Image bool `json:"image"` |
| 83 | } |
| 84 | |
| 85 | // PromptCapabilities returns the prompt capabilities the agent declared, and |
| 86 | // the zero value — nothing beyond text — when it declared none. |
| 87 | func (r InitializeResult) PromptCapabilities() PromptCapabilities { |
| 88 | var capabilities struct { |
| 89 | Prompt PromptCapabilities `json:"promptCapabilities"` |
| 90 | } |
| 91 | if len(r.AgentCapabilities) > 0 { |
| 92 | _ = json.Unmarshal(r.AgentCapabilities, &capabilities) |
| 93 | } |
| 94 | return capabilities.Prompt |
| 95 | } |
| 96 | |
| 97 | // NewSessionParams starts a conversation in a directory. |
| 98 | // |
| 99 | // McpServers is always empty and is still sent: the field is required, and the |
| 100 | // MCP servers an agent talks to are its own configuration's business. |
| 101 | type NewSessionParams struct { |
| 102 | Cwd string `json:"cwd"` |
| 103 | McpServers []any `json:"mcpServers"` |
| 104 | } |
| 105 | |
| 106 | // NewSessionResult carries the id every later message quotes. |
| 107 | type NewSessionResult struct { |
| 108 | SessionID string `json:"sessionId"` |
| 109 | } |
| 110 | |
| 111 | // The kinds of content block this editor sends. An agent may send others |
| 112 | // back — image, audio — and those are drawn as nothing rather than refused. |
| 113 | const ( |
| 114 | ContentText = "text" |
| 115 | ContentResourceLink = "resource_link" |
| 116 | ContentResource = "resource" |
| 117 | ) |
| 118 | |
| 119 | // ContentBlock is one piece of a message. |
| 120 | // |
| 121 | // Three shapes share the struct, told apart by Type: text carries Text; a |
| 122 | // resource_link carries URI, Name and MimeType and points at a file the agent |
| 123 | // fetches for itself; a resource carries the file's text inside Resource, for |
| 124 | // an agent that accepts embedded context. The last two are how a file named |
| 125 | // with "@" in the box reaches the agent. |
| 126 | type ContentBlock struct { |
| 127 | Type string `json:"type"` |
| 128 | Text string `json:"text,omitempty"` |
| 129 | |
| 130 | URI string `json:"uri,omitempty"` |
| 131 | Name string `json:"name,omitempty"` |
| 132 | MimeType string `json:"mimeType,omitempty"` |
| 133 | |
| 134 | Resource *EmbeddedResource `json:"resource,omitempty"` |
| 135 | } |
| 136 | |
| 137 | // EmbeddedResource is a file's text carried inside the prompt. |
| 138 | type EmbeddedResource struct { |
| 139 | URI string `json:"uri"` |
| 140 | MimeType string `json:"mimeType,omitempty"` |
| 141 | Text string `json:"text"` |
| 142 | } |
| 143 | |
| 144 | // Text returns the block's text, and "" for a block that carries none. |
| 145 | func (c ContentBlock) String() string { return c.Text } |
| 146 | |
| 147 | // PromptParams sends one turn's worth of input. |
| 148 | type PromptParams struct { |
| 149 | SessionID string `json:"sessionId"` |
| 150 | Prompt []ContentBlock `json:"prompt"` |
| 151 | } |
| 152 | |
| 153 | // The reasons a turn ends. |
| 154 | const ( |
| 155 | StopEndTurn = "end_turn" |
| 156 | StopMaxTokens = "max_tokens" |
| 157 | StopMaxTurnRequests = "max_turn_requests" |
| 158 | StopRefusal = "refusal" |
| 159 | StopCancelled = "cancelled" |
| 160 | ) |
| 161 | |
| 162 | // PromptResult says why the turn ended. |
| 163 | type PromptResult struct { |
| 164 | StopReason string `json:"stopReason"` |
| 165 | } |
| 166 | |
| 167 | // CancelParams interrupts a turn. It is a notification: there is nothing to |
| 168 | // answer. |
| 169 | type CancelParams struct { |
| 170 | SessionID string `json:"sessionId"` |
| 171 | } |
| 172 | |
| 173 | // The kinds of session/update an agent sends. |
| 174 | const ( |
| 175 | UpdateAgentMessage = "agent_message_chunk" |
| 176 | UpdateAgentThought = "agent_thought_chunk" |
| 177 | UpdateUserMessage = "user_message_chunk" |
| 178 | UpdateToolCall = "tool_call" |
| 179 | UpdateToolCallDone = "tool_call_update" |
| 180 | UpdatePlan = "plan" |
| 181 | UpdateCommands = "available_commands_update" |
| 182 | UpdateUsage = "usage_update" |
| 183 | UpdateCurrentMode = "current_mode_update" |
| 184 | ) |
| 185 | |
| 186 | // The statuses a tool call moves through. |
| 187 | const ( |
| 188 | StatusPending = "pending" |
| 189 | StatusInProgress = "in_progress" |
| 190 | StatusCompleted = "completed" |
| 191 | StatusFailed = "failed" |
| 192 | ) |
| 193 | |
| 194 | // PlanEntry is one line of a plan the agent published. |
| 195 | type PlanEntry struct { |
| 196 | Content string `json:"content"` |
| 197 | Priority string `json:"priority,omitempty"` |
| 198 | Status string `json:"status,omitempty"` |
| 199 | } |
| 200 | |
| 201 | // Command is one thing the agent says it can be asked to do: a slash command, |
| 202 | // typed as "/name" at the start of a prompt, the way Zed and the other clients |
| 203 | // send it. The protocol has no method for it — a command is a text prompt the |
| 204 | // agent recognises by its first word. |
| 205 | type Command struct { |
| 206 | Name string `json:"name"` |
| 207 | Description string `json:"description,omitempty"` |
| 208 | Input *CommandInput `json:"input,omitempty"` |
| 209 | } |
| 210 | |
| 211 | // CommandInput says a command wants something after its name, and hints at |
| 212 | // what: "query to search for", "description of what to plan". |
| 213 | // |
| 214 | // It is a pointer on Command because its absence is the fact that matters — |
| 215 | // a command with no input is complete once its name is typed — and an empty |
| 216 | // struct cannot be told from a missing one. |
| 217 | type CommandInput struct { |
| 218 | Hint string `json:"hint,omitempty"` |
| 219 | } |
| 220 | |
| 221 | // TakesInput reports whether the command wants something typed after its name. |
| 222 | // |
| 223 | // acp.Command{Name: "test"}.TakesInput() // false |
| 224 | // acp.Command{Name: "web", Input: &acp.CommandInput{}}.TakesInput() // true |
| 225 | func (c Command) TakesInput() bool { return c.Input != nil } |
| 226 | |
| 227 | // Hint returns what the agent suggests typing after the name, or "" when the |
| 228 | // command takes nothing, or the agent did not say. |
| 229 | func (c Command) Hint() string { |
| 230 | if c.Input == nil { |
| 231 | return "" |
| 232 | } |
| 233 | return c.Input.Hint |
| 234 | } |
| 235 | |
| 236 | // ToolCallContent is one piece of what a tool produced. |
| 237 | // |
| 238 | // The protocol gives it three shapes — a content block, a diff, and a |
| 239 | // reference to a terminal — told apart by Type. Only the first is drawn; |
| 240 | // the others are named so that an unknown one can be reported rather than |
| 241 | // silently dropped. |
| 242 | type ToolCallContent struct { |
| 243 | Type string `json:"type"` |
| 244 | Content ContentBlock `json:"content"` |
| 245 | Path string `json:"path,omitempty"` |
| 246 | OldText string `json:"oldText,omitempty"` |
| 247 | NewText string `json:"newText,omitempty"` |
| 248 | } |
| 249 | |
| 250 | // Update is one session/update, in every shape it comes in. |
| 251 | // |
| 252 | // Content is kept encoded because the protocol uses the same name for two |
| 253 | // different things: a single block on a message chunk, and an array of |
| 254 | // ToolCallContent on a tool call. Decoding it eagerly into either would break |
| 255 | // on the other, which is a defect that only shows up once an agent uses a tool. |
| 256 | type Update struct { |
| 257 | SessionUpdate string `json:"sessionUpdate"` |
| 258 | Content json.RawMessage `json:"content,omitempty"` |
| 259 | |
| 260 | ToolCallID string `json:"toolCallId,omitempty"` |
| 261 | Title string `json:"title,omitempty"` |
| 262 | Kind string `json:"kind,omitempty"` |
| 263 | Status string `json:"status,omitempty"` |
| 264 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 265 | |
| 266 | Entries []PlanEntry `json:"entries,omitempty"` |
| 267 | AvailableCommands []Command `json:"availableCommands,omitempty"` |
| 268 | |
| 269 | Used int64 `json:"used,omitempty"` |
| 270 | Size int64 `json:"size,omitempty"` |
| 271 | } |
| 272 | |
| 273 | // Block returns Content read as a single content block, for the message and |
| 274 | // thought chunks that carry one. |
| 275 | func (u Update) Block() ContentBlock { |
| 276 | var block ContentBlock |
| 277 | if len(u.Content) == 0 { |
| 278 | return block |
| 279 | } |
| 280 | _ = json.Unmarshal(u.Content, &block) |
| 281 | return block |
| 282 | } |
| 283 | |
| 284 | // Blocks returns Content read as a tool call's output, which is an array. |
| 285 | func (u Update) Blocks() []ToolCallContent { |
| 286 | var blocks []ToolCallContent |
| 287 | if len(u.Content) == 0 { |
| 288 | return nil |
| 289 | } |
| 290 | _ = json.Unmarshal(u.Content, &blocks) |
| 291 | return blocks |
| 292 | } |
| 293 | |
| 294 | // SessionNotification wraps every update in the session it belongs to. |
| 295 | type SessionNotification struct { |
| 296 | SessionID string `json:"sessionId"` |
| 297 | Update Update `json:"update"` |
| 298 | } |
| 299 | |
| 300 | // ToolCallUpdate is the tool a permission request is about. |
| 301 | type ToolCallUpdate struct { |
| 302 | ToolCallID string `json:"toolCallId,omitempty"` |
| 303 | Title string `json:"title,omitempty"` |
| 304 | Kind string `json:"kind,omitempty"` |
| 305 | Status string `json:"status,omitempty"` |
| 306 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 307 | } |
| 308 | |
| 309 | // PermissionOption is one answer the agent will accept. |
| 310 | // |
| 311 | // Kind is a hint about what the option means — allow_once, allow_always, |
| 312 | // reject_once, reject_always — and is what lets Escape be mapped onto the |
| 313 | // agent's own idea of "no" instead of a guess. |
| 314 | type PermissionOption struct { |
| 315 | OptionID string `json:"optionId"` |
| 316 | Name string `json:"name"` |
| 317 | Kind string `json:"kind,omitempty"` |
| 318 | } |
| 319 | |
| 320 | // The permission option kinds the protocol defines. |
| 321 | const ( |
| 322 | OptionAllowOnce = "allow_once" |
| 323 | OptionAllowAlways = "allow_always" |
| 324 | OptionRejectOnce = "reject_once" |
| 325 | OptionRejectAlways = "reject_always" |
| 326 | ) |
| 327 | |
| 328 | // RequestPermissionParams is the agent asking before it acts. |
| 329 | type RequestPermissionParams struct { |
| 330 | SessionID string `json:"sessionId"` |
| 331 | ToolCall ToolCallUpdate `json:"toolCall"` |
| 332 | Options []PermissionOption `json:"options"` |
| 333 | } |
| 334 | |
| 335 | // The outcomes a permission request can end in. |
| 336 | const ( |
| 337 | OutcomeSelected = "selected" |
| 338 | OutcomeCancelled = "cancelled" |
| 339 | ) |
| 340 | |
| 341 | // PermissionOutcome is the answer: which option, or that the turn was |
| 342 | // interrupted before anybody chose. |
| 343 | type PermissionOutcome struct { |
| 344 | Outcome string `json:"outcome"` |
| 345 | OptionID string `json:"optionId,omitempty"` |
| 346 | } |
| 347 | |
| 348 | // RequestPermissionResult wraps the outcome, as the protocol asks. |
| 349 | type RequestPermissionResult struct { |
| 350 | Outcome PermissionOutcome `json:"outcome"` |
| 351 | } |
| 352 | |
| 353 | // ReadTextFileParams is the agent asking the editor for a file's text. |
| 354 | type ReadTextFileParams struct { |
| 355 | SessionID string `json:"sessionId"` |
| 356 | Path string `json:"path"` |
| 357 | Line *int `json:"line,omitempty"` |
| 358 | Limit *int `json:"limit,omitempty"` |
| 359 | } |
| 360 | |
| 361 | // ReadTextFileResult carries the text back. |
| 362 | type ReadTextFileResult struct { |
| 363 | Content string `json:"content"` |
| 364 | } |
| 365 | |
| 366 | // WriteTextFileParams is the agent asking the editor to write a file. |
| 367 | type WriteTextFileParams struct { |
| 368 | SessionID string `json:"sessionId"` |
| 369 | Path string `json:"path"` |
| 370 | Content string `json:"content"` |
| 371 | } |