// Package acp implements the Agent Client Protocol (https://agentclientprotocol.com) // transport: a stdio JSON-RPC 2.0 agent that editors and other host clients speak // to drive Reasonix. Many tools integrated with the v1 (main-branch) agent over // ACP, so v2 keeps the wire contract identical — the wire types in this file are a // faithful port of main's src/acp/protocol.ts (ACP protocol version 1). // // The package is an adapter layer over the v2 kernel and depends only on stable // contracts: it maps the agent's typed event.Event stream onto session/update // notifications (see dispatch.go), bridges permission.Approver onto // session/request_permission round-trips (see permission.go), and exposes the // whole thing over NDJSON JSON-RPC (see server.go). How a per-session agent is // actually assembled — provider, tools rooted at the session cwd, per-session MCP // — is left to a Factory the composition root supplies (see service.go), so this // package stays independent of the cli wiring. package acp import ( "encoding/json" "fmt" "strings" ) // ProtocolVersion is the ACP version this agent implements. Matches main. const ProtocolVersion = 1 // JSON-RPC 2.0 error codes (subset used on the wire). Mirrors protocol.ts. const ( ErrParse = -32700 ErrInvalidRequest = -32600 ErrMethodNotFound = -32601 ErrInvalidParams = -32602 ErrInternal = -32603 ) // --- initialize --- // InitializeParams is the client's handshake. The agent records the client's // capabilities — fs read/write proxying and host terminals are used when // offered — and advertises its own fixed capability set in reply. type InitializeParams struct { ProtocolVersion int `json:"protocolVersion"` ClientInfo *Implementation `json:"clientInfo,omitempty"` ClientCapabilities ClientCapabilities `json:"clientCapabilities,omitempty"` } // ClientCapabilities is what the client offers the agent: filesystem proxy // methods (fs/read_text_file, fs/write_text_file) that see unsaved editor // buffers, and host-owned terminals (terminal/*). type ClientCapabilities struct { FS FSCapabilities `json:"fs,omitempty"` Terminal bool `json:"terminal,omitempty"` } // FSCapabilities reports which client filesystem methods are available. type FSCapabilities struct { ReadTextFile bool `json:"readTextFile,omitempty"` WriteTextFile bool `json:"writeTextFile,omitempty"` } // Implementation names a participant (client or agent) on the wire. type Implementation struct { Name string `json:"name"` Title string `json:"title,omitempty"` Version string `json:"version,omitempty"` } // InitializeResult advertises what this agent supports: persisted session load, // ACP v1 session lifecycle helpers, inline resource text (embeddedContext) but // not image/audio, and stdio / Streamable HTTP MCP (no legacy sse). type InitializeResult struct { ProtocolVersion int `json:"protocolVersion"` AgentCapabilities AgentCapabilities `json:"agentCapabilities"` AgentInfo Implementation `json:"agentInfo"` AuthMethods []AuthMethod `json:"authMethods"` } // AgentCapabilities is the agentCapabilities object in InitializeResult. type AgentCapabilities struct { LoadSession bool `json:"loadSession"` SessionCapabilities SessionCapabilities `json:"sessionCapabilities,omitempty"` PromptCapabilities PromptCapabilities `json:"promptCapabilities"` MCPCapabilities MCPCapabilities `json:"mcpCapabilities"` } // EmptyCapability serializes to {} for ACP capability flags. type EmptyCapability struct{} // SessionCapabilities advertises optional session lifecycle methods. type SessionCapabilities struct { List *EmptyCapability `json:"list,omitempty"` Resume *EmptyCapability `json:"resume,omitempty"` Close *EmptyCapability `json:"close,omitempty"` Delete *EmptyCapability `json:"delete,omitempty"` } // PromptCapabilities reports which content-block kinds prompts may carry. type PromptCapabilities struct { Image bool `json:"image"` Audio bool `json:"audio"` EmbeddedContext bool `json:"embeddedContext"` } // MCPCapabilities reports which MCP transports session/new accepts. type MCPCapabilities struct { HTTP bool `json:"http"` SSE bool `json:"sse"` } // AuthMethod advertises how a client can prepare credentials for the agent. type AuthMethod struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` Type string `json:"type,omitempty"` Args []string `json:"args,omitempty"` Env map[string]string `json:"env,omitempty"` } // AuthenticateParams selects one advertised auth method. Terminal methods are // normally handled by the client by launching the agent with the method's args; // accepting this request keeps clients that call authenticate directly working. type AuthenticateParams struct { MethodID string `json:"methodId"` } // AuthenticateResult is the empty authentication ack. type AuthenticateResult struct{} // --- session/new --- // SessionNewParams opens a session rooted at cwd, optionally with MCP servers // the agent should connect for the session's lifetime. type SessionNewParams struct { Cwd string `json:"cwd,omitempty"` MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` } // MCPServerSpec describes one MCP server the client asks the agent to connect. type MCPServerSpec struct { Name string `json:"name"` Type string `json:"type,omitempty"` Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Env MCPEnv `json:"env,omitempty"` URL string `json:"url,omitempty"` Headers MCPHeaders `json:"headers,omitempty"` } // MCPEnv accepts ACP's official EnvVariable[] shape while still accepting the // older map shape that Reasonix v1 clients used. type MCPEnv map[string]string // MCPHeaders accepts ACP's official HTTPHeader[] shape while still accepting // the older map shape that Reasonix v1 clients used. The official spec // (https://agentclientprotocol.com) ships HTTP/SSE MCP headers as an array of // {name,value} objects, even when empty. type MCPHeaders map[string]string // EnvVariable is one official ACP MCP environment variable entry. The same // {name,value} shape is also used by HTTP/SSE headers in the ACP spec, so we // reuse it as the parse target for [MCPHeaders] too. type EnvVariable struct { Name string `json:"name"` Value string `json:"value"` } func (e *MCPEnv) UnmarshalJSON(raw []byte) error { out, err := unmarshalNameValueMap(raw, "env") if err != nil { return err } *e = out return nil } func (h *MCPHeaders) UnmarshalJSON(raw []byte) error { out, err := unmarshalNameValueMap(raw, "headers") if err != nil { return err } *h = out return nil } // unmarshalNameValueMap parses ACP's official [{name,value}, ...] array shape // or the legacy {name: value, ...} map shape into a map. field names the JSON // field for error messages. func unmarshalNameValueMap(raw []byte, field string) (map[string]string, error) { if s := strings.TrimSpace(string(raw)); s == "" || s == "null" { return nil, nil } var vars []EnvVariable if err := json.Unmarshal(raw, &vars); err == nil { out := make(map[string]string, len(vars)) for i, v := range vars { if strings.TrimSpace(v.Name) == "" { return nil, fmt.Errorf("%s[%d].name is required", field, i) } out[v.Name] = v.Value } return out, nil } var legacy map[string]string if err := json.Unmarshal(raw, &legacy); err == nil { return legacy, nil } return nil, fmt.Errorf("%s must be an array of {name,value} objects", field) } // SessionNewResult returns the opaque id used to address the session thereafter. type SessionNewResult struct { SessionID string `json:"sessionId"` Models *SessionModelState `json:"models,omitempty"` Modes *SessionModeState `json:"modes,omitempty"` ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` } // --- session modes --- // SessionMode is one operating mode the client can switch the session into. type SessionMode struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` } // SessionModeState reports the current mode and the full mode list. type SessionModeState struct { CurrentModeID string `json:"currentModeId"` AvailableModes []SessionMode `json:"availableModes"` } // SessionSetModeParams switches a session's operating mode. type SessionSetModeParams struct { SessionID string `json:"sessionId"` ModeID string `json:"modeId"` } // SessionSetModeResult is the empty ack. type SessionSetModeResult struct{} // ModelInfo describes one selectable model in ACP's legacy model selector. type ModelInfo struct { ModelID string `json:"modelId"` Name string `json:"name"` Description string `json:"description,omitempty"` } // SessionModelState is ACP's legacy model selector state. New clients should // prefer the category:"model" config option, but some hosts still probe this. type SessionModelState struct { AvailableModels []ModelInfo `json:"availableModels"` CurrentModelID string `json:"currentModelId"` } // --- session/load --- // SessionLoadParams resumes a session saved under sessionId (the id a prior // session/new returned), optionally re-rooting it at cwd with fresh MCP servers. // The agent replays the stored conversation as session/update notifications // before the request returns. type SessionLoadParams struct { SessionID string `json:"sessionId"` Cwd string `json:"cwd,omitempty"` MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` } // SessionLoadResult is the empty ack; the conversation has already arrived as a // burst of session/update notifications by the time it is sent. type SessionLoadResult struct { Models *SessionModelState `json:"models,omitempty"` Modes *SessionModeState `json:"modes,omitempty"` ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` } // --- session/resume --- // SessionResumeParams resumes a session without replaying its transcript. type SessionResumeParams struct { SessionID string `json:"sessionId"` Cwd string `json:"cwd,omitempty"` MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` } // SessionResumeResult is the empty ack returned once the session is ready. type SessionResumeResult struct { Models *SessionModelState `json:"models,omitempty"` Modes *SessionModeState `json:"modes,omitempty"` ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` } // --- session/set_config_option --- // SetSessionConfigOptionParams changes one advertised session config option. type SetSessionConfigOptionParams struct { SessionID string `json:"sessionId"` ConfigID string `json:"configId"` Value string `json:"value"` } // SetSessionConfigOptionResult returns the full refreshed config state. type SetSessionConfigOptionResult struct { ConfigOptions []SessionConfigOption `json:"configOptions"` } // SessionConfigOption is a single-value ACP session selector. type SessionConfigOption struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` Category string `json:"category,omitempty"` Type string `json:"type"` CurrentValue string `json:"currentValue"` Options []SessionConfigSelectOption `json:"options"` } // SessionConfigSelectOption is one selectable value for a config option. type SessionConfigSelectOption struct { Value string `json:"value"` Name string `json:"name"` Description string `json:"description,omitempty"` } // --- session/set_model --- // SetSessionModelParams is ACP's legacy model-switching request. type SetSessionModelParams struct { SessionID string `json:"sessionId"` ModelID string `json:"modelId"` } // SetSessionModelResult is the empty ack for legacy model switching. type SetSessionModelResult struct{} // --- session/list --- // SessionListParams lists known sessions, optionally filtered by cwd. type SessionListParams struct { Cwd string `json:"cwd,omitempty"` Cursor string `json:"cursor,omitempty"` } // SessionListResult is the first and only page of sessions Reasonix currently // returns. NextCursor is omitted because the in-process list is unpaged. type SessionListResult struct { Sessions []SessionInfo `json:"sessions"` NextCursor string `json:"nextCursor,omitempty"` } // SessionInfo is the ACP session/list item shape. type SessionInfo struct { SessionID string `json:"sessionId"` Cwd string `json:"cwd"` Title string `json:"title,omitempty"` UpdatedAt string `json:"updatedAt,omitempty"` Meta map[string]any `json:"_meta,omitempty"` } // --- session/close --- // SessionCloseParams closes an active session and releases its resources. type SessionCloseParams struct { SessionID string `json:"sessionId"` } // SessionCloseResult is the empty close ack. type SessionCloseResult struct{} // --- session/delete --- // SessionDeleteParams removes a session from future session/list results. type SessionDeleteParams struct { SessionID string `json:"sessionId"` } // SessionDeleteResult is the empty delete ack. type SessionDeleteResult struct{} // --- content blocks (inbound prompt) --- // ContentBlock is one piece of a prompt. The agent reads text blocks and the // inline text of resource blocks (embeddedContext); image/audio are accepted on // the wire but ignored, matching the advertised capabilities. type ContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` Resource *ResourceContents `json:"resource,omitempty"` MimeType string `json:"mimeType,omitempty"` Data string `json:"data,omitempty"` } // ResourceContents is the embedded resource of a "resource" content block. type ResourceContents struct { URI string `json:"uri"` MimeType string `json:"mimeType,omitempty"` Text string `json:"text,omitempty"` } // FlattenPrompt extracts the user-visible prompt text out of ACP content blocks. // Text blocks contribute their text; resource blocks contribute their inline // text when present (embeddedContext). Other block kinds are dropped. Ported from // protocol.ts flattenPrompt. func FlattenPrompt(blocks []ContentBlock) string { parts := make([]string, 0, len(blocks)) for _, b := range blocks { switch b.Type { case "text": if b.Text != "" { parts = append(parts, b.Text) } case "resource": if b.Resource != nil && b.Resource.Text != "" { parts = append(parts, b.Resource.Text) } } } return strings.TrimSpace(strings.Join(parts, "\n\n")) } // --- session/prompt --- // SessionPromptParams sends a turn's prompt to a session. type SessionPromptParams struct { SessionID string `json:"sessionId"` Prompt []ContentBlock `json:"prompt"` } // StopReason tells the client why a turn ended. Values match main's wire. type StopReason string const ( StopEndTurn StopReason = "end_turn" StopCancelled StopReason = "cancelled" StopError StopReason = "error" ) // SessionPromptResult ends a session/prompt. TranscriptPath is reserved for a // future on-disk transcript pointer; omitted (null) for now. type SessionPromptResult struct { StopReason StopReason `json:"stopReason"` TranscriptPath *string `json:"transcriptPath,omitempty"` } // --- session/update (agent → client notifications) --- // // SessionUpdate is a tagged union discriminated by sessionUpdate. The variants // reuse the JSON key "content" with two incompatible shapes (a single block for // message chunks, an array for tool results), so we model each variant as its own // struct rather than one struct with conflicting tags, and carry it through // SessionUpdateParams.Update as an interface value. // SessionUpdateParams wraps one update for a session. type SessionUpdateParams struct { SessionID string `json:"sessionId"` Update any `json:"update"` } // messageChunk is agent_message_chunk / agent_thought_chunk. type messageChunk struct { SessionUpdate string `json:"sessionUpdate"` Content ContentBlock `json:"content"` Metadata *updateMeta `json:"metadata,omitempty"` } // updateMeta carries optional error detail on an agent_message_chunk. type updateMeta struct { Error *updateError `json:"error,omitempty"` } type updateError struct { Name string `json:"name"` Message string `json:"message"` } // toolCall is a "tool_call" update (announces a call, with title/kind/rawInput). type toolCall struct { SessionUpdate string `json:"sessionUpdate"` 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"` Locations []ToolCallLocation `json:"locations,omitempty"` } // ToolCallLocation names a file (and optionally a line) a tool call touches, so // the client can follow along in the editor. type ToolCallLocation struct { Path string `json:"path"` Line *int `json:"line,omitempty"` } // toolCallUpdateMsg is a "tool_call_update" update (status + result content). type toolCallUpdateMsg struct { SessionUpdate string `json:"sessionUpdate"` ToolCallID string `json:"toolCallId"` Status string `json:"status,omitempty"` Content []toolContent `json:"content,omitempty"` } // toolContent wraps a tool result's text, per the ACP tool_call_update shape. type toolContent struct { Type string `json:"type"` Content ContentBlock `json:"content"` } // availableCommandsUpdate advertises slash commands that the ACP client may // surface in its composer. The client sends invocations back as normal // session/prompt text such as "/review diff". type availableCommandsUpdate struct { SessionUpdate string `json:"sessionUpdate"` AvailableCommands []AvailableCommand `json:"availableCommands"` } // AvailableCommand is one slash command available in a session. type AvailableCommand struct { Name string `json:"name"` Description string `json:"description"` Input *AvailableCommandInput `json:"input,omitempty"` } // AvailableCommandInput describes a command's free-form text argument. type AvailableCommandInput struct { Hint string `json:"hint"` } // configOptionUpdate reports a complete refreshed session config state. type configOptionUpdate struct { SessionUpdate string `json:"sessionUpdate"` ConfigOptions []SessionConfigOption `json:"configOptions"` } // planUpdate is a "plan" update: the agent's current task list. Each update // carries the complete plan and replaces the previous one, mirroring the // todo_write contract it is derived from. type planUpdate struct { SessionUpdate string `json:"sessionUpdate"` Entries []PlanEntry `json:"entries"` } // PlanEntry is one task in a plan update. type PlanEntry struct { Content string `json:"content"` Priority string `json:"priority"` Status string `json:"status"` } // currentModeUpdate reports that the session switched operating modes. type currentModeUpdate struct { SessionUpdate string `json:"sessionUpdate"` CurrentModeID string `json:"currentModeId"` } // --- fs/* (agent → client requests) --- // FSReadTextFileParams asks the client for a file's current text, including // unsaved editor state. Line (1-based) and Limit page the content; Reasonix // always reads whole files and pages locally, so it sends neither. type FSReadTextFileParams struct { SessionID string `json:"sessionId"` Path string `json:"path"` Line *int `json:"line,omitempty"` Limit *int `json:"limit,omitempty"` } // FSReadTextFileResult carries the file content. type FSReadTextFileResult struct { Content string `json:"content"` } // FSWriteTextFileParams asks the client to write content to path, updating any // open buffer as well as the file on disk. type FSWriteTextFileParams struct { SessionID string `json:"sessionId"` Path string `json:"path"` Content string `json:"content"` } // --- terminal/* (agent → client requests) --- // TerminalCreateParams starts a command in a client-owned terminal. type TerminalCreateParams struct { SessionID string `json:"sessionId"` Command string `json:"command"` Args []string `json:"args,omitempty"` Cwd string `json:"cwd,omitempty"` OutputByteLimit int `json:"outputByteLimit,omitempty"` } // TerminalCreateResult returns the id used by the other terminal methods. type TerminalCreateResult struct { TerminalID string `json:"terminalId"` } // TerminalIDParams addresses one terminal (output / kill / wait / release). type TerminalIDParams struct { SessionID string `json:"sessionId"` TerminalID string `json:"terminalId"` } // TerminalOutputResult is the terminal's captured output so far. type TerminalOutputResult struct { Output string `json:"output"` Truncated bool `json:"truncated"` ExitStatus *TerminalExitStatus `json:"exitStatus,omitempty"` } // TerminalWaitResult reports how the command exited. type TerminalWaitResult struct { ExitCode *int `json:"exitCode,omitempty"` Signal *string `json:"signal,omitempty"` } // TerminalExitStatus mirrors TerminalWaitResult inside terminal/output. type TerminalExitStatus struct { ExitCode *int `json:"exitCode,omitempty"` Signal *string `json:"signal,omitempty"` } // --- session/cancel (client → agent notification) --- // SessionCancelParams cancels an in-progress turn. type SessionCancelParams struct { SessionID string `json:"sessionId"` } // --- session/request_permission (agent → client request) --- // PermissionOptionKind classifies an option for host UI styling. It is an ACP v1 // wire enum, so host-visible permission choices must stay within the official // protocol values. type PermissionOptionKind string const ( OptAllowOnce PermissionOptionKind = "allow_once" OptAllowAlways PermissionOptionKind = "allow_always" OptRejectOnce PermissionOptionKind = "reject_once" OptRejectAlways PermissionOptionKind = "reject_always" ) // PermissionOption is one choice offered to the user for a permission request. type PermissionOption struct { OptionID string `json:"optionId"` Name string `json:"name"` Kind PermissionOptionKind `json:"kind"` } // PermissionRequestParams asks the client to approve a pending tool call. type PermissionRequestParams struct { SessionID string `json:"sessionId"` ToolCall PermissionToolCall `json:"toolCall"` Options []PermissionOption `json:"options"` } // PermissionToolCall describes the call awaiting approval. type PermissionToolCall struct { ToolCallID string `json:"toolCallId"` Title string `json:"title,omitempty"` Kind string `json:"kind,omitempty"` Status string `json:"status,omitempty"` Content []toolContent `json:"content,omitempty"` RawInput json.RawMessage `json:"rawInput,omitempty"` } // PermissionRequestResult is the client's reply to a permission request. type PermissionRequestResult struct { Outcome PermissionOutcome `json:"outcome"` } // PermissionOutcome is "selected" (with optionId) or "cancelled". type PermissionOutcome struct { Outcome string `json:"outcome"` OptionID string `json:"optionId,omitempty"` }