| 1 | // Package acp implements the Agent Client Protocol (https://agentclientprotocol.com) |
| 2 | // transport: a stdio JSON-RPC 2.0 agent that editors and other host clients speak |
| 3 | // to drive Reasonix. Many tools integrated with the v1 (main-branch) agent over |
| 4 | // ACP, so v2 keeps the wire contract identical — the wire types in this file are a |
| 5 | // faithful port of main's src/acp/protocol.ts (ACP protocol version 1). |
| 6 | // |
| 7 | // The package is an adapter layer over the v2 kernel and depends only on stable |
| 8 | // contracts: it maps the agent's typed event.Event stream onto session/update |
| 9 | // notifications (see dispatch.go), bridges permission.Approver onto |
| 10 | // session/request_permission round-trips (see permission.go), and exposes the |
| 11 | // whole thing over NDJSON JSON-RPC (see server.go). How a per-session agent is |
| 12 | // actually assembled — provider, tools rooted at the session cwd, per-session MCP |
| 13 | // — is left to a Factory the composition root supplies (see service.go), so this |
| 14 | // package stays independent of the cli wiring. |
| 15 | package acp |
| 16 | |
| 17 | import ( |
| 18 | "encoding/json" |
| 19 | "fmt" |
| 20 | "strings" |
| 21 | ) |
| 22 | |
| 23 | // ProtocolVersion is the ACP version this agent implements. Matches main. |
| 24 | const ProtocolVersion = 1 |
| 25 | |
| 26 | // JSON-RPC 2.0 error codes (subset used on the wire). Mirrors protocol.ts. |
| 27 | const ( |
| 28 | ErrParse = -32700 |
| 29 | ErrInvalidRequest = -32600 |
| 30 | ErrMethodNotFound = -32601 |
| 31 | ErrInvalidParams = -32602 |
| 32 | ErrInternal = -32603 |
| 33 | ) |
| 34 | |
| 35 | // --- initialize --- |
| 36 | |
| 37 | // InitializeParams is the client's handshake. The agent records the client's |
| 38 | // capabilities — fs read/write proxying and host terminals are used when |
| 39 | // offered — and advertises its own fixed capability set in reply. |
| 40 | type InitializeParams struct { |
| 41 | ProtocolVersion int `json:"protocolVersion"` |
| 42 | ClientInfo *Implementation `json:"clientInfo,omitempty"` |
| 43 | ClientCapabilities ClientCapabilities `json:"clientCapabilities,omitempty"` |
| 44 | } |
| 45 | |
| 46 | // ClientCapabilities is what the client offers the agent: filesystem proxy |
| 47 | // methods (fs/read_text_file, fs/write_text_file) that see unsaved editor |
| 48 | // buffers, and host-owned terminals (terminal/*). Meta carries vendor |
| 49 | // capability blocks (e.g. _meta["reasonix.io"]) for tolerant parse — unknown |
| 50 | // or malformed entries simply mean the vendor feature stays off. |
| 51 | type ClientCapabilities struct { |
| 52 | FS FSCapabilities `json:"fs,omitempty"` |
| 53 | Terminal bool `json:"terminal,omitempty"` |
| 54 | Meta map[string]any `json:"_meta,omitempty"` |
| 55 | } |
| 56 | |
| 57 | // FSCapabilities reports which client filesystem methods are available. |
| 58 | type FSCapabilities struct { |
| 59 | ReadTextFile bool `json:"readTextFile,omitempty"` |
| 60 | WriteTextFile bool `json:"writeTextFile,omitempty"` |
| 61 | } |
| 62 | |
| 63 | // Implementation names a participant (client or agent) on the wire. |
| 64 | type Implementation struct { |
| 65 | Name string `json:"name"` |
| 66 | Title string `json:"title,omitempty"` |
| 67 | Version string `json:"version,omitempty"` |
| 68 | } |
| 69 | |
| 70 | // InitializeResult advertises what this agent supports: persisted session load, |
| 71 | // ACP v1 session lifecycle helpers, inline resource text (embeddedContext) but |
| 72 | // not image/audio, and stdio / Streamable HTTP MCP (no legacy sse). |
| 73 | type InitializeResult struct { |
| 74 | ProtocolVersion int `json:"protocolVersion"` |
| 75 | AgentCapabilities AgentCapabilities `json:"agentCapabilities"` |
| 76 | AgentInfo Implementation `json:"agentInfo"` |
| 77 | AuthMethods []AuthMethod `json:"authMethods"` |
| 78 | } |
| 79 | |
| 80 | // AgentCapabilities is the agentCapabilities object in InitializeResult. |
| 81 | type AgentCapabilities struct { |
| 82 | LoadSession bool `json:"loadSession"` |
| 83 | SessionCapabilities SessionCapabilities `json:"sessionCapabilities,omitempty"` |
| 84 | PromptCapabilities PromptCapabilities `json:"promptCapabilities"` |
| 85 | MCPCapabilities MCPCapabilities `json:"mcpCapabilities"` |
| 86 | Meta map[string]any `json:"_meta,omitempty"` |
| 87 | } |
| 88 | |
| 89 | // ReasonixExtensionCapabilities advertises Reasonix-specific ACP extensions. |
| 90 | // ACP v1 reserves agentCapabilities._meta for vendor capability discovery. |
| 91 | type ReasonixExtensionCapabilities struct { |
| 92 | SessionSteer *SessionSteerCapability `json:"sessionSteer,omitempty"` |
| 93 | // SessionReloadExtensions advertises the vendor runtime-reload method. |
| 94 | SessionReloadExtensions *SessionReloadExtensionsCapability `json:"sessionReloadExtensions,omitempty"` |
| 95 | // ExtensionSurface advertises structured extension-UI surface support: |
| 96 | // the agent publishes surfaces as vendor session/update payloads. |
| 97 | ExtensionSurface *ExtensionSurfaceCapability `json:"extensionSurface,omitempty"` |
| 98 | } |
| 99 | |
| 100 | // SessionSteerCapability identifies the vendor-namespaced steering method. |
| 101 | type SessionSteerCapability struct { |
| 102 | Method string `json:"method"` |
| 103 | } |
| 104 | |
| 105 | // SessionReloadExtensionsCapability identifies the vendor-namespaced runtime |
| 106 | // reload method. |
| 107 | type SessionReloadExtensionsCapability struct { |
| 108 | Method string `json:"method"` |
| 109 | } |
| 110 | |
| 111 | const ( |
| 112 | // reasonixExtensionSurfaceSchemaVersion versions the extension-surface DTO |
| 113 | // carried by the vendor session/update variant. |
| 114 | reasonixExtensionSurfaceSchemaVersion = 1 |
| 115 | // extensionSurfaceUpdateKind discriminates the vendor session/update |
| 116 | // variant that carries a structured extension-UI surface. |
| 117 | extensionSurfaceUpdateKind = "_reasonix.io/extension_surface" |
| 118 | ) |
| 119 | |
| 120 | // ExtensionSurfaceCapability advertises that a participant renders structured |
| 121 | // extension-UI surfaces (Extension Protocol v1) natively. |
| 122 | type ExtensionSurfaceCapability struct { |
| 123 | Supported bool `json:"supported"` |
| 124 | SchemaVersion int `json:"schemaVersion"` |
| 125 | } |
| 126 | |
| 127 | // EmptyCapability serializes to {} for ACP capability flags. |
| 128 | type EmptyCapability struct{} |
| 129 | |
| 130 | // SessionCapabilities advertises optional session lifecycle methods. |
| 131 | type SessionCapabilities struct { |
| 132 | List *EmptyCapability `json:"list,omitempty"` |
| 133 | Resume *EmptyCapability `json:"resume,omitempty"` |
| 134 | Close *EmptyCapability `json:"close,omitempty"` |
| 135 | Delete *EmptyCapability `json:"delete,omitempty"` |
| 136 | } |
| 137 | |
| 138 | // PromptCapabilities reports which content-block kinds prompts may carry. |
| 139 | type PromptCapabilities struct { |
| 140 | Image bool `json:"image"` |
| 141 | Audio bool `json:"audio"` |
| 142 | EmbeddedContext bool `json:"embeddedContext"` |
| 143 | } |
| 144 | |
| 145 | // MCPCapabilities reports which MCP transports session/new accepts. |
| 146 | type MCPCapabilities struct { |
| 147 | HTTP bool `json:"http"` |
| 148 | SSE bool `json:"sse"` |
| 149 | } |
| 150 | |
| 151 | // AuthMethod advertises how a client can prepare credentials for the agent. |
| 152 | type AuthMethod struct { |
| 153 | ID string `json:"id"` |
| 154 | Name string `json:"name"` |
| 155 | Description string `json:"description,omitempty"` |
| 156 | Type string `json:"type,omitempty"` |
| 157 | Args []string `json:"args,omitempty"` |
| 158 | Env map[string]string `json:"env,omitempty"` |
| 159 | } |
| 160 | |
| 161 | // AuthenticateParams selects one advertised auth method. Terminal methods are |
| 162 | // normally handled by the client by launching the agent with the method's args; |
| 163 | // accepting this request keeps clients that call authenticate directly working. |
| 164 | type AuthenticateParams struct { |
| 165 | MethodID string `json:"methodId"` |
| 166 | } |
| 167 | |
| 168 | // AuthenticateResult is the empty authentication ack. |
| 169 | type AuthenticateResult struct{} |
| 170 | |
| 171 | // --- session/new --- |
| 172 | |
| 173 | // SessionNewParams opens a session rooted at cwd, optionally with MCP servers |
| 174 | // the agent should connect for the session's lifetime. |
| 175 | type SessionNewParams struct { |
| 176 | Cwd string `json:"cwd,omitempty"` |
| 177 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 178 | } |
| 179 | |
| 180 | // MCPServerSpec describes one MCP server the client asks the agent to connect. |
| 181 | type MCPServerSpec struct { |
| 182 | Name string `json:"name"` |
| 183 | Type string `json:"type,omitempty"` |
| 184 | Command string `json:"command,omitempty"` |
| 185 | Args []string `json:"args,omitempty"` |
| 186 | Env MCPEnv `json:"env,omitempty"` |
| 187 | URL string `json:"url,omitempty"` |
| 188 | Headers MCPHeaders `json:"headers,omitempty"` |
| 189 | } |
| 190 | |
| 191 | // MCPEnv accepts ACP's official EnvVariable[] shape while still accepting the |
| 192 | // older map shape that Reasonix v1 clients used. |
| 193 | type MCPEnv map[string]string |
| 194 | |
| 195 | // MCPHeaders accepts ACP's official HTTPHeader[] shape while still accepting |
| 196 | // the older map shape that Reasonix v1 clients used. The official spec |
| 197 | // (https://agentclientprotocol.com) ships HTTP/SSE MCP headers as an array of |
| 198 | // {name,value} objects, even when empty. |
| 199 | type MCPHeaders map[string]string |
| 200 | |
| 201 | // EnvVariable is one official ACP MCP environment variable entry. The same |
| 202 | // {name,value} shape is also used by HTTP/SSE headers in the ACP spec, so we |
| 203 | // reuse it as the parse target for [MCPHeaders] too. |
| 204 | type EnvVariable struct { |
| 205 | Name string `json:"name"` |
| 206 | Value string `json:"value"` |
| 207 | } |
| 208 | |
| 209 | func (e *MCPEnv) UnmarshalJSON(raw []byte) error { |
| 210 | out, err := unmarshalNameValueMap(raw, "env") |
| 211 | if err != nil { |
| 212 | return err |
| 213 | } |
| 214 | *e = out |
| 215 | return nil |
| 216 | } |
| 217 | |
| 218 | func (h *MCPHeaders) UnmarshalJSON(raw []byte) error { |
| 219 | out, err := unmarshalNameValueMap(raw, "headers") |
| 220 | if err != nil { |
| 221 | return err |
| 222 | } |
| 223 | *h = out |
| 224 | return nil |
| 225 | } |
| 226 | |
| 227 | // unmarshalNameValueMap parses ACP's official [{name,value}, ...] array shape |
| 228 | // or the legacy {name: value, ...} map shape into a map. field names the JSON |
| 229 | // field for error messages. |
| 230 | func unmarshalNameValueMap(raw []byte, field string) (map[string]string, error) { |
| 231 | if s := strings.TrimSpace(string(raw)); s == "" || s == "null" { |
| 232 | return nil, nil |
| 233 | } |
| 234 | |
| 235 | var vars []EnvVariable |
| 236 | if err := json.Unmarshal(raw, &vars); err == nil { |
| 237 | out := make(map[string]string, len(vars)) |
| 238 | for i, v := range vars { |
| 239 | if strings.TrimSpace(v.Name) == "" { |
| 240 | return nil, fmt.Errorf("%s[%d].name is required", field, i) |
| 241 | } |
| 242 | out[v.Name] = v.Value |
| 243 | } |
| 244 | return out, nil |
| 245 | } |
| 246 | |
| 247 | var legacy map[string]string |
| 248 | if err := json.Unmarshal(raw, &legacy); err == nil { |
| 249 | return legacy, nil |
| 250 | } |
| 251 | return nil, fmt.Errorf("%s must be an array of {name,value} objects", field) |
| 252 | } |
| 253 | |
| 254 | // SessionNewResult returns the opaque id used to address the session thereafter. |
| 255 | type SessionNewResult struct { |
| 256 | SessionID string `json:"sessionId"` |
| 257 | Models *SessionModelState `json:"models,omitempty"` |
| 258 | Modes *SessionModeState `json:"modes,omitempty"` |
| 259 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 260 | } |
| 261 | |
| 262 | // --- session modes --- |
| 263 | |
| 264 | // SessionMode is one operating mode the client can switch the session into. |
| 265 | type SessionMode struct { |
| 266 | ID string `json:"id"` |
| 267 | Name string `json:"name"` |
| 268 | Description string `json:"description,omitempty"` |
| 269 | } |
| 270 | |
| 271 | // SessionModeState reports the current mode and the full mode list. |
| 272 | type SessionModeState struct { |
| 273 | CurrentModeID string `json:"currentModeId"` |
| 274 | AvailableModes []SessionMode `json:"availableModes"` |
| 275 | } |
| 276 | |
| 277 | // SessionSetModeParams switches a session's operating mode. |
| 278 | type SessionSetModeParams struct { |
| 279 | SessionID string `json:"sessionId"` |
| 280 | ModeID string `json:"modeId"` |
| 281 | } |
| 282 | |
| 283 | // SessionSetModeResult is the empty ack. |
| 284 | type SessionSetModeResult struct{} |
| 285 | |
| 286 | // ModelInfo describes one selectable model in ACP's legacy model selector. |
| 287 | type ModelInfo struct { |
| 288 | ModelID string `json:"modelId"` |
| 289 | Name string `json:"name"` |
| 290 | Description string `json:"description,omitempty"` |
| 291 | } |
| 292 | |
| 293 | // SessionModelState is ACP's legacy model selector state. New clients should |
| 294 | // prefer the category:"model" config option, but some hosts still probe this. |
| 295 | type SessionModelState struct { |
| 296 | AvailableModels []ModelInfo `json:"availableModels"` |
| 297 | CurrentModelID string `json:"currentModelId"` |
| 298 | } |
| 299 | |
| 300 | // --- session/load --- |
| 301 | |
| 302 | // SessionLoadParams resumes a session saved under sessionId (the id a prior |
| 303 | // session/new returned), optionally re-rooting it at cwd with fresh MCP servers. |
| 304 | // The agent replays the stored conversation as session/update notifications |
| 305 | // before the request returns. |
| 306 | type SessionLoadParams struct { |
| 307 | SessionID string `json:"sessionId"` |
| 308 | Cwd string `json:"cwd,omitempty"` |
| 309 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 310 | } |
| 311 | |
| 312 | // SessionLoadResult is the empty ack; the conversation has already arrived as a |
| 313 | // burst of session/update notifications by the time it is sent. |
| 314 | type SessionLoadResult struct { |
| 315 | Models *SessionModelState `json:"models,omitempty"` |
| 316 | Modes *SessionModeState `json:"modes,omitempty"` |
| 317 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 318 | } |
| 319 | |
| 320 | // --- session/resume --- |
| 321 | |
| 322 | // SessionResumeParams resumes a session without replaying its transcript. |
| 323 | type SessionResumeParams struct { |
| 324 | SessionID string `json:"sessionId"` |
| 325 | Cwd string `json:"cwd,omitempty"` |
| 326 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 327 | } |
| 328 | |
| 329 | // SessionResumeResult is the empty ack returned once the session is ready. |
| 330 | type SessionResumeResult struct { |
| 331 | Models *SessionModelState `json:"models,omitempty"` |
| 332 | Modes *SessionModeState `json:"modes,omitempty"` |
| 333 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 334 | } |
| 335 | |
| 336 | // --- session/set_config_option --- |
| 337 | |
| 338 | // SetSessionConfigOptionParams changes one advertised session config option. |
| 339 | type SetSessionConfigOptionParams struct { |
| 340 | SessionID string `json:"sessionId"` |
| 341 | ConfigID string `json:"configId"` |
| 342 | Value string `json:"value"` |
| 343 | } |
| 344 | |
| 345 | // SetSessionConfigOptionResult returns the full refreshed config state. |
| 346 | type SetSessionConfigOptionResult struct { |
| 347 | ConfigOptions []SessionConfigOption `json:"configOptions"` |
| 348 | } |
| 349 | |
| 350 | // SessionConfigOption is a single-value ACP session selector. |
| 351 | type SessionConfigOption struct { |
| 352 | ID string `json:"id"` |
| 353 | Name string `json:"name"` |
| 354 | Description string `json:"description,omitempty"` |
| 355 | Category string `json:"category,omitempty"` |
| 356 | Type string `json:"type"` |
| 357 | CurrentValue string `json:"currentValue"` |
| 358 | Options []SessionConfigSelectOption `json:"options"` |
| 359 | } |
| 360 | |
| 361 | // SessionConfigSelectOption is one selectable value for a config option. |
| 362 | type SessionConfigSelectOption struct { |
| 363 | Value string `json:"value"` |
| 364 | Name string `json:"name"` |
| 365 | Description string `json:"description,omitempty"` |
| 366 | } |
| 367 | |
| 368 | // --- session/set_model --- |
| 369 | |
| 370 | // SetSessionModelParams is ACP's legacy model-switching request. |
| 371 | type SetSessionModelParams struct { |
| 372 | SessionID string `json:"sessionId"` |
| 373 | ModelID string `json:"modelId"` |
| 374 | } |
| 375 | |
| 376 | // SetSessionModelResult is the empty ack for legacy model switching. |
| 377 | type SetSessionModelResult struct{} |
| 378 | |
| 379 | // --- session/list --- |
| 380 | |
| 381 | // SessionListParams lists known sessions, optionally filtered by cwd. |
| 382 | type SessionListParams struct { |
| 383 | Cwd string `json:"cwd,omitempty"` |
| 384 | Cursor string `json:"cursor,omitempty"` |
| 385 | } |
| 386 | |
| 387 | // SessionListResult is the first and only page of sessions Reasonix currently |
| 388 | // returns. NextCursor is omitted because the in-process list is unpaged. |
| 389 | type SessionListResult struct { |
| 390 | Sessions []SessionInfo `json:"sessions"` |
| 391 | NextCursor string `json:"nextCursor,omitempty"` |
| 392 | } |
| 393 | |
| 394 | // SessionInfo is the ACP session/list item shape. |
| 395 | type SessionInfo struct { |
| 396 | SessionID string `json:"sessionId"` |
| 397 | Cwd string `json:"cwd"` |
| 398 | Title string `json:"title,omitempty"` |
| 399 | UpdatedAt string `json:"updatedAt,omitempty"` |
| 400 | Meta map[string]any `json:"_meta,omitempty"` |
| 401 | } |
| 402 | |
| 403 | // --- session/close --- |
| 404 | |
| 405 | // SessionCloseParams closes an active session and releases its resources. |
| 406 | type SessionCloseParams struct { |
| 407 | SessionID string `json:"sessionId"` |
| 408 | } |
| 409 | |
| 410 | // SessionCloseResult is the empty close ack. |
| 411 | type SessionCloseResult struct{} |
| 412 | |
| 413 | // --- session/delete --- |
| 414 | |
| 415 | // SessionDeleteParams removes a session from future session/list results. |
| 416 | type SessionDeleteParams struct { |
| 417 | SessionID string `json:"sessionId"` |
| 418 | } |
| 419 | |
| 420 | // SessionDeleteResult is the empty delete ack. |
| 421 | type SessionDeleteResult struct{} |
| 422 | |
| 423 | // --- content blocks (inbound prompt) --- |
| 424 | |
| 425 | // ContentBlock is one piece of a prompt. The agent reads text blocks and the |
| 426 | // inline text of resource blocks (embeddedContext); image/audio are accepted on |
| 427 | // the wire but ignored, matching the advertised capabilities. |
| 428 | type ContentBlock struct { |
| 429 | Type string `json:"type"` |
| 430 | Text string `json:"text,omitempty"` |
| 431 | Resource *ResourceContents `json:"resource,omitempty"` |
| 432 | MimeType string `json:"mimeType,omitempty"` |
| 433 | Data string `json:"data,omitempty"` |
| 434 | } |
| 435 | |
| 436 | // ResourceContents is the embedded resource of a "resource" content block. |
| 437 | type ResourceContents struct { |
| 438 | URI string `json:"uri"` |
| 439 | MimeType string `json:"mimeType,omitempty"` |
| 440 | Text string `json:"text,omitempty"` |
| 441 | } |
| 442 | |
| 443 | // FlattenPrompt extracts the user-visible prompt text out of ACP content blocks. |
| 444 | // Text blocks contribute their text; resource blocks contribute their inline |
| 445 | // text when present (embeddedContext). Other block kinds are dropped. Ported from |
| 446 | // protocol.ts flattenPrompt. |
| 447 | func FlattenPrompt(blocks []ContentBlock) string { |
| 448 | parts := make([]string, 0, len(blocks)) |
| 449 | for _, b := range blocks { |
| 450 | switch b.Type { |
| 451 | case "text": |
| 452 | if b.Text != "" { |
| 453 | parts = append(parts, b.Text) |
| 454 | } |
| 455 | case "resource": |
| 456 | if b.Resource != nil && b.Resource.Text != "" { |
| 457 | parts = append(parts, b.Resource.Text) |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | return strings.TrimSpace(strings.Join(parts, "\n\n")) |
| 462 | } |
| 463 | |
| 464 | // --- session/prompt --- |
| 465 | |
| 466 | // SessionPromptParams sends a turn's prompt to a session. |
| 467 | type SessionPromptParams struct { |
| 468 | SessionID string `json:"sessionId"` |
| 469 | Prompt []ContentBlock `json:"prompt"` |
| 470 | } |
| 471 | |
| 472 | // SessionSteerParams is the Reasonix ACP v1 extension for injecting user |
| 473 | // guidance into an active prompt without cancelling it. |
| 474 | type SessionSteerParams struct { |
| 475 | SessionID string `json:"sessionId"` |
| 476 | Prompt []ContentBlock `json:"prompt"` |
| 477 | } |
| 478 | |
| 479 | // SessionSteerResult acknowledges that the active turn accepted the guidance. |
| 480 | type SessionSteerResult struct{} |
| 481 | |
| 482 | // sessionSteerMethod follows ACP v1's reserved vendor-extension namespace. |
| 483 | const sessionSteerMethod = "_reasonix.io/session/steer" |
| 484 | |
| 485 | // SessionReloadExtensionsParams addresses one live ACP session. |
| 486 | type SessionReloadExtensionsParams struct { |
| 487 | SessionID string `json:"sessionId"` |
| 488 | } |
| 489 | |
| 490 | // SessionReloadExtensionsResult reports whether the runtime reload ran |
| 491 | // immediately (Queued false) or was coalesced behind a turn/rebuild in flight |
| 492 | // to run when the session goes idle (Queued true). |
| 493 | type SessionReloadExtensionsResult struct { |
| 494 | Queued bool `json:"queued,omitempty"` |
| 495 | } |
| 496 | |
| 497 | // sessionReloadExtensionsMethod follows ACP v1's reserved vendor-extension |
| 498 | // namespace, like sessionSteerMethod: only the "_<vendor>/" prefix is reserved |
| 499 | // for vendor methods, so the bare "reasonix/session/reloadExtensions" form |
| 500 | // could collide with a future official ACP method and must not be used. |
| 501 | const sessionReloadExtensionsMethod = "_reasonix.io/session/reloadExtensions" |
| 502 | |
| 503 | // StopReason tells the client why a turn ended. Values match main's wire. |
| 504 | type StopReason string |
| 505 | |
| 506 | const ( |
| 507 | StopEndTurn StopReason = "end_turn" |
| 508 | StopCancelled StopReason = "cancelled" |
| 509 | StopError StopReason = "error" |
| 510 | ) |
| 511 | |
| 512 | // SessionPromptResult ends a session/prompt. TranscriptPath is reserved for a |
| 513 | // future on-disk transcript pointer; omitted (null) for now. |
| 514 | type SessionPromptResult struct { |
| 515 | StopReason StopReason `json:"stopReason"` |
| 516 | TranscriptPath *string `json:"transcriptPath,omitempty"` |
| 517 | } |
| 518 | |
| 519 | // --- session/update (agent → client notifications) --- |
| 520 | // |
| 521 | // SessionUpdate is a tagged union discriminated by sessionUpdate. The variants |
| 522 | // reuse the JSON key "content" with two incompatible shapes (a single block for |
| 523 | // message chunks, an array for tool results), so we model each variant as its own |
| 524 | // struct rather than one struct with conflicting tags, and carry it through |
| 525 | // SessionUpdateParams.Update as an interface value. |
| 526 | |
| 527 | // SessionUpdateParams wraps one update for a session. |
| 528 | type SessionUpdateParams struct { |
| 529 | SessionID string `json:"sessionId"` |
| 530 | Update any `json:"update"` |
| 531 | } |
| 532 | |
| 533 | // messageChunk is agent_message_chunk / agent_thought_chunk. |
| 534 | type messageChunk struct { |
| 535 | SessionUpdate string `json:"sessionUpdate"` |
| 536 | Content ContentBlock `json:"content"` |
| 537 | Metadata *updateMeta `json:"metadata,omitempty"` |
| 538 | } |
| 539 | |
| 540 | // extensionSurfaceUpdate is the vendor session/update variant that carries one |
| 541 | // structured extension-UI surface to clients that negotiated |
| 542 | // reasonix.extensionSurface in initialize. ACP has no standard notification for |
| 543 | // extension surfaces, so the DTO (the shared eventwire JSON contract) rides |
| 544 | // _meta["reasonix.io"]["extensionSurface"], mirroring how the initialize |
| 545 | // handshake namespaces vendor data under "reasonix.io". The sink always pairs |
| 546 | // it with a flattened agent_message_chunk text fallback (belt and suspenders): |
| 547 | // a client that ignores the vendor variant still shows the content. |
| 548 | type extensionSurfaceUpdate struct { |
| 549 | SessionUpdate string `json:"sessionUpdate"` |
| 550 | Meta map[string]any `json:"_meta"` |
| 551 | } |
| 552 | |
| 553 | // updateMeta carries optional error detail on an agent_message_chunk. |
| 554 | type updateMeta struct { |
| 555 | Error *updateError `json:"error,omitempty"` |
| 556 | } |
| 557 | |
| 558 | type updateError struct { |
| 559 | Name string `json:"name"` |
| 560 | Message string `json:"message"` |
| 561 | } |
| 562 | |
| 563 | // toolCall is a "tool_call" update (announces a call, with title/kind/rawInput). |
| 564 | type toolCall struct { |
| 565 | SessionUpdate string `json:"sessionUpdate"` |
| 566 | ToolCallID string `json:"toolCallId"` |
| 567 | Title string `json:"title,omitempty"` |
| 568 | Kind string `json:"kind,omitempty"` |
| 569 | Status string `json:"status,omitempty"` |
| 570 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 571 | Locations []ToolCallLocation `json:"locations,omitempty"` |
| 572 | } |
| 573 | |
| 574 | // ToolCallLocation names a file (and optionally a line) a tool call touches, so |
| 575 | // the client can follow along in the editor. |
| 576 | type ToolCallLocation struct { |
| 577 | Path string `json:"path"` |
| 578 | Line *int `json:"line,omitempty"` |
| 579 | } |
| 580 | |
| 581 | // toolCallUpdateMsg is a "tool_call_update" update (status + result content). |
| 582 | type toolCallUpdateMsg struct { |
| 583 | SessionUpdate string `json:"sessionUpdate"` |
| 584 | ToolCallID string `json:"toolCallId"` |
| 585 | Status string `json:"status,omitempty"` |
| 586 | Content []toolContent `json:"content,omitempty"` |
| 587 | } |
| 588 | |
| 589 | // toolContent wraps a tool result's text, per the ACP tool_call_update shape. |
| 590 | type toolContent struct { |
| 591 | Type string `json:"type"` |
| 592 | Content ContentBlock `json:"content"` |
| 593 | } |
| 594 | |
| 595 | // availableCommandsUpdate advertises slash commands that the ACP client may |
| 596 | // surface in its composer. The client sends invocations back as normal |
| 597 | // session/prompt text such as "/review diff". |
| 598 | type availableCommandsUpdate struct { |
| 599 | SessionUpdate string `json:"sessionUpdate"` |
| 600 | AvailableCommands []AvailableCommand `json:"availableCommands"` |
| 601 | } |
| 602 | |
| 603 | // AvailableCommand is one slash command available in a session. |
| 604 | type AvailableCommand struct { |
| 605 | Name string `json:"name"` |
| 606 | Description string `json:"description"` |
| 607 | Input *AvailableCommandInput `json:"input,omitempty"` |
| 608 | } |
| 609 | |
| 610 | // AvailableCommandInput describes a command's free-form text argument. |
| 611 | type AvailableCommandInput struct { |
| 612 | Hint string `json:"hint"` |
| 613 | } |
| 614 | |
| 615 | // configOptionUpdate reports a complete refreshed session config state. |
| 616 | type configOptionUpdate struct { |
| 617 | SessionUpdate string `json:"sessionUpdate"` |
| 618 | ConfigOptions []SessionConfigOption `json:"configOptions"` |
| 619 | } |
| 620 | |
| 621 | // planUpdate is a "plan" update: the agent's current task list. Each update |
| 622 | // carries the complete plan and replaces the previous one, mirroring the |
| 623 | // todo_write contract it is derived from. |
| 624 | type planUpdate struct { |
| 625 | SessionUpdate string `json:"sessionUpdate"` |
| 626 | Entries []PlanEntry `json:"entries"` |
| 627 | } |
| 628 | |
| 629 | // PlanEntry is one task in a plan update. |
| 630 | type PlanEntry struct { |
| 631 | Content string `json:"content"` |
| 632 | Priority string `json:"priority"` |
| 633 | Status string `json:"status"` |
| 634 | } |
| 635 | |
| 636 | // currentModeUpdate reports that the session switched operating modes. |
| 637 | type currentModeUpdate struct { |
| 638 | SessionUpdate string `json:"sessionUpdate"` |
| 639 | CurrentModeID string `json:"currentModeId"` |
| 640 | } |
| 641 | |
| 642 | // --- fs/* (agent → client requests) --- |
| 643 | |
| 644 | // FSReadTextFileParams asks the client for a file's current text, including |
| 645 | // unsaved editor state. Line (1-based) and Limit page the content; Reasonix |
| 646 | // always reads whole files and pages locally, so it sends neither. |
| 647 | type FSReadTextFileParams struct { |
| 648 | SessionID string `json:"sessionId"` |
| 649 | Path string `json:"path"` |
| 650 | Line *int `json:"line,omitempty"` |
| 651 | Limit *int `json:"limit,omitempty"` |
| 652 | } |
| 653 | |
| 654 | // FSReadTextFileResult carries the file content. |
| 655 | type FSReadTextFileResult struct { |
| 656 | Content string `json:"content"` |
| 657 | } |
| 658 | |
| 659 | // FSWriteTextFileParams asks the client to write content to path, updating any |
| 660 | // open buffer as well as the file on disk. |
| 661 | type FSWriteTextFileParams struct { |
| 662 | SessionID string `json:"sessionId"` |
| 663 | Path string `json:"path"` |
| 664 | Content string `json:"content"` |
| 665 | } |
| 666 | |
| 667 | // --- terminal/* (agent → client requests) --- |
| 668 | |
| 669 | // TerminalCreateParams starts a command in a client-owned terminal. |
| 670 | // Env follows ACP v1's official EnvVariable[] shape (same as MCP env): only |
| 671 | // the overrides Reasonix owns (typically TMPDIR/TMP/TEMP) are sent — never a |
| 672 | // full host environment dump. |
| 673 | type TerminalCreateParams struct { |
| 674 | SessionID string `json:"sessionId"` |
| 675 | Command string `json:"command"` |
| 676 | Args []string `json:"args,omitempty"` |
| 677 | Cwd string `json:"cwd,omitempty"` |
| 678 | Env []EnvVariable `json:"env,omitempty"` |
| 679 | OutputByteLimit int `json:"outputByteLimit,omitempty"` |
| 680 | } |
| 681 | |
| 682 | // TerminalCreateResult returns the id used by the other terminal methods. |
| 683 | type TerminalCreateResult struct { |
| 684 | TerminalID string `json:"terminalId"` |
| 685 | } |
| 686 | |
| 687 | // TerminalIDParams addresses one terminal (output / kill / wait / release). |
| 688 | type TerminalIDParams struct { |
| 689 | SessionID string `json:"sessionId"` |
| 690 | TerminalID string `json:"terminalId"` |
| 691 | } |
| 692 | |
| 693 | // TerminalOutputResult is the terminal's captured output so far. |
| 694 | type TerminalOutputResult struct { |
| 695 | Output string `json:"output"` |
| 696 | Truncated bool `json:"truncated"` |
| 697 | ExitStatus *TerminalExitStatus `json:"exitStatus,omitempty"` |
| 698 | } |
| 699 | |
| 700 | // TerminalWaitResult reports how the command exited. |
| 701 | type TerminalWaitResult struct { |
| 702 | ExitCode *int `json:"exitCode,omitempty"` |
| 703 | Signal *string `json:"signal,omitempty"` |
| 704 | } |
| 705 | |
| 706 | // TerminalExitStatus mirrors TerminalWaitResult inside terminal/output. |
| 707 | type TerminalExitStatus struct { |
| 708 | ExitCode *int `json:"exitCode,omitempty"` |
| 709 | Signal *string `json:"signal,omitempty"` |
| 710 | } |
| 711 | |
| 712 | // --- session/cancel (client → agent notification) --- |
| 713 | |
| 714 | // SessionCancelParams cancels an in-progress turn. |
| 715 | type SessionCancelParams struct { |
| 716 | SessionID string `json:"sessionId"` |
| 717 | } |
| 718 | |
| 719 | // --- session/request_permission (agent → client request) --- |
| 720 | |
| 721 | // PermissionOptionKind classifies an option for host UI styling. It is an ACP v1 |
| 722 | // wire enum, so host-visible permission choices must stay within the official |
| 723 | // protocol values. |
| 724 | type PermissionOptionKind string |
| 725 | |
| 726 | const ( |
| 727 | OptAllowOnce PermissionOptionKind = "allow_once" |
| 728 | OptAllowAlways PermissionOptionKind = "allow_always" |
| 729 | OptRejectOnce PermissionOptionKind = "reject_once" |
| 730 | OptRejectAlways PermissionOptionKind = "reject_always" |
| 731 | ) |
| 732 | |
| 733 | // PermissionOption is one choice offered to the user for a permission request. |
| 734 | type PermissionOption struct { |
| 735 | OptionID string `json:"optionId"` |
| 736 | Name string `json:"name"` |
| 737 | Kind PermissionOptionKind `json:"kind"` |
| 738 | } |
| 739 | |
| 740 | // PermissionRequestParams asks the client to approve a pending tool call. |
| 741 | type PermissionRequestParams struct { |
| 742 | SessionID string `json:"sessionId"` |
| 743 | ToolCall PermissionToolCall `json:"toolCall"` |
| 744 | Options []PermissionOption `json:"options"` |
| 745 | } |
| 746 | |
| 747 | // PermissionToolCall describes the call awaiting approval. |
| 748 | type PermissionToolCall struct { |
| 749 | ToolCallID string `json:"toolCallId"` |
| 750 | Title string `json:"title,omitempty"` |
| 751 | Kind string `json:"kind,omitempty"` |
| 752 | Status string `json:"status,omitempty"` |
| 753 | Content []toolContent `json:"content,omitempty"` |
| 754 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 755 | Locations []ToolCallLocation `json:"locations,omitempty"` |
| 756 | Meta map[string]any `json:"_meta,omitempty"` |
| 757 | } |
| 758 | |
| 759 | // PermissionRequestResult is the client's reply to a permission request. |
| 760 | type PermissionRequestResult struct { |
| 761 | Outcome PermissionOutcome `json:"outcome"` |
| 762 | } |
| 763 | |
| 764 | // PermissionOutcome is "selected" (with optionId) or "cancelled". |
| 765 | type PermissionOutcome struct { |
| 766 | Outcome string `json:"outcome"` |
| 767 | OptionID string `json:"optionId,omitempty"` |
| 768 | } |
| 769 |