package acp import "encoding/json" // ProtocolVersion is the ACP revision this package speaks. T3 Code sends 1 in // its initialize request and every other client we know of does too; a client // that asks for something else still gets 1 back, because answering with a // version we do not implement would be a lie the handshake cannot recover // from. const ProtocolVersion = 1 // The method names, spelled once. A typo in a string literal is a method the // peer never hears about and an error nobody can grep for. const ( MethodInitialize = "initialize" MethodAuthenticate = "authenticate" MethodNewSession = "session/new" MethodPrompt = "session/prompt" MethodCancel = "session/cancel" MethodSessionUpdate = "session/update" MethodRequestPermission = "session/request_permission" ) // Implementation names a peer: the agent to the client, or the client to the // agent. type Implementation struct { Name string `json:"name"` Title string `json:"title,omitempty"` Version string `json:"version"` } // InitializeRequest is the first thing a client sends. // // ClientCapabilities is kept raw. We advertise no filesystem or terminal // capability of our own, so there is nothing in there we act on, and decoding // a structure we ignore is a decode that can fail for no reason. type InitializeRequest struct { ProtocolVersion int `json:"protocolVersion"` ClientCapabilities json.RawMessage `json:"clientCapabilities,omitempty"` ClientInfo *Implementation `json:"clientInfo,omitempty"` } // InitializeResponse answers it. Only ProtocolVersion is required by the // protocol; everything else is how the client decides what to ask us for // later, so leaving a field out is the way to say "do not ask". type InitializeResponse struct { ProtocolVersion int `json:"protocolVersion"` AgentInfo *Implementation `json:"agentInfo,omitempty"` AgentCapabilities *AgentCapabilities `json:"agentCapabilities,omitempty"` // AuthMethods is serialized even when empty: a client that sees the key // knows we answered the question, where a missing key means "unstated". AuthMethods []AuthMethod `json:"authMethods"` // Meta is the protocol's escape hatch, and the one thing we put in it is // ModelStateMeta. A client probing which models an agent has does not want // to open a session to find out, and answering here is what lets its model // picker be filled from the endpoint rather than from parsing our own // command-line output. Meta map[string]any `json:"_meta,omitempty"` } // ModelStateMeta is the key under InitializeResponse.Meta that carries a // [SessionModelState]. const ModelStateMeta = "modelState" // AgentCapabilities is what the agent can be asked to do beyond one prompt. type AgentCapabilities struct { LoadSession bool `json:"loadSession,omitempty"` PromptCapabilities *PromptCapabilities `json:"promptCapabilities,omitempty"` } // PromptCapabilities is what a prompt may contain. type PromptCapabilities struct { Image bool `json:"image,omitempty"` Audio bool `json:"audio,omitempty"` EmbeddedContext bool `json:"embeddedContext,omitempty"` } // AuthMethod is one way of logging in that the agent offers. We offer none — // Codey takes its credential from the environment — so this exists to make the // empty list well typed rather than to be constructed. type AuthMethod struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` } // AuthenticateRequest asks the agent to log in with one of the methods it // advertised. Clients send it unconditionally, including a method id we never // offered, so the id is informational. type AuthenticateRequest struct { MethodID string `json:"methodId"` } // NewSessionRequest opens a conversation rooted at a directory. // // AdditionalDirectories is not in every published copy of the schema but T3 // Code sends it, and McpServers is accepted and ignored: an agent that refuses // to decode a field it does not use is an agent that breaks on the client's // next release. type NewSessionRequest struct { Cwd string `json:"cwd"` McpServers []json.RawMessage `json:"mcpServers,omitempty"` AdditionalDirectories []string `json:"additionalDirectories,omitempty"` } // NewSessionResponse hands back the id every later message carries. type NewSessionResponse struct { SessionID string `json:"sessionId"` Models *SessionModelState `json:"models,omitempty"` } // SessionModelState is the model picker: what is selected, and what else there // is. A client that gets this uses it in preference to anything the agent // printed on a command line. type SessionModelState struct { CurrentModelID string `json:"currentModelId"` AvailableModels []ModelInfo `json:"availableModels"` } // ModelInfo is one entry in that picker. type ModelInfo struct { ModelID string `json:"modelId"` Name string `json:"name"` Description string `json:"description,omitempty"` } // SetModelRequest selects a model mid-session. type SetModelRequest struct { SessionID string `json:"sessionId"` ModelID string `json:"modelId"` } // PromptRequest is one user turn. type PromptRequest struct { SessionID string `json:"sessionId"` Prompt []ContentBlock `json:"prompt"` } // ContentBlock is a piece of a message. The union is decoded into one struct // because Go has no sum types and the alternative — a custom unmarshaller per // variant — buys nothing when the only variant we read is text. type ContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` Data string `json:"data,omitempty"` MimeType string `json:"mimeType,omitempty"` URI string `json:"uri,omitempty"` Name string `json:"name,omitempty"` } // The stop reasons a prompt can end with. const ( StopEndTurn = "end_turn" StopCancelled = "cancelled" StopRefusal = "refusal" StopMaxTokens = "max_tokens" StopMaxTurnRequests = "max_turn_requests" ) // PromptResponse ends the turn. StopReason is the only field the clients we // know of read. type PromptResponse struct { StopReason string `json:"stopReason"` } // CancelParams asks for the turn in flight to stop. It is a notification, so // there is nothing to answer: the acknowledgement is the prompt coming back // with StopCancelled. type CancelParams struct { SessionID string `json:"sessionId"` } // SessionNotification is everything the agent says while a turn runs. One // struct with a tagged Update, because that is how the wire is shaped. type SessionNotification struct { SessionID string `json:"sessionId"` Update Update `json:"update"` } // The sessionUpdate discriminators we emit. const ( UpdateAgentMessageChunk = "agent_message_chunk" UpdateAgentThoughtChunk = "agent_thought_chunk" UpdateToolCall = "tool_call" UpdateToolCallUpdate = "tool_call_update" ) // Update is the tagged body of a session notification. Every field beyond // SessionUpdate is optional, and which ones are read depends on the tag. // // Content is `any` because the wire reuses one key for two shapes: on a chunk // it is a single content block, on a tool call it is a list of tool call // content. Updates are only ever written, never decoded, so carrying the union // as an interface costs nothing and spells out that the shape follows the tag. type Update struct { SessionUpdate string `json:"sessionUpdate"` Content any `json:"content,omitempty"` // Set on the tool-call variants. ToolCallID is what pairs an update with // the call it belongs to, so it is the one field that is never optional // there. ToolCallID string `json:"toolCallId,omitempty"` Title string `json:"title,omitempty"` Kind string `json:"kind,omitempty"` Status string `json:"status,omitempty"` RawInput json.RawMessage `json:"rawInput,omitempty"` } // The tool kinds ACP defines. A client uses them to pick an icon and to decide // what to fold away, so a wrong one is cosmetic and a missing one is fine. const ( KindRead = "read" KindEdit = "edit" KindDelete = "delete" KindMove = "move" KindSearch = "search" KindExecute = "execute" KindThink = "think" KindFetch = "fetch" KindOther = "other" ) // The tool call statuses. const ( StatusPending = "pending" StatusInProgress = "in_progress" StatusCompleted = "completed" StatusFailed = "failed" ) // ToolCallContent is what a tool produced: text, or a diff a client can render // side by side. type ToolCallContent struct { Type string `json:"type"` Content *ContentBlock `json:"content,omitempty"` // Set when Type is "diff". Path string `json:"path,omitempty"` OldText string `json:"oldText,omitempty"` NewText string `json:"newText,omitempty"` } // TextContent is the content block a chunk carries, which is always text in // what Codey emits. func TextContent(text string) *ContentBlock { return &ContentBlock{Type: "text", Text: text} } // ToolCallText wraps tool output as tool call content. func ToolCallText(text string) ToolCallContent { return ToolCallContent{Type: "content", Content: TextContent(text)} } // RequestPermissionParams asks the client to decide about one tool call. type RequestPermissionParams struct { SessionID string `json:"sessionId"` ToolCall PermissionToolCall `json:"toolCall"` Options []PermissionOption `json:"options"` } // PermissionToolCall describes what is waiting on the answer. type PermissionToolCall struct { ToolCallID string `json:"toolCallId"` Title string `json:"title,omitempty"` Kind string `json:"kind,omitempty"` Status string `json:"status,omitempty"` RawInput json.RawMessage `json:"rawInput,omitempty"` } // PermissionOption is one answer the client may give. type PermissionOption struct { OptionID string `json:"optionId"` Name string `json:"name"` Kind string `json:"kind"` } // The option kinds, and the option ids we use. // // The ids are not ours to invent freely. Cursor's adapter in T3 Code replies // with these three literals regardless of what the agent offered, so choosing // them here is what lets the same binary sit in a Cursor-shaped slot as well // as a Grok-shaped one. const ( KindAllowOnce = "allow_once" KindAllowAlways = "allow_always" KindRejectOnce = "reject_once" OptAllowOnce = "allow-once" OptAllowAlways = "allow-always" OptRejectOnce = "reject-once" ) // RequestPermissionResult is the answer. type RequestPermissionResult struct { Outcome PermissionOutcome `json:"outcome"` } // PermissionOutcome is either a selection or a cancellation. type PermissionOutcome struct { Outcome string `json:"outcome"` OptionID string `json:"optionId,omitempty"` } // The outcome tags. const ( OutcomeSelected = "selected" OutcomeCancelled = "cancelled" )