| 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 | // SessionSteerCapability identifies the vendor-namespaced steering method. |
| 90 | type SessionSteerCapability struct { |
| 91 | Method string `json:"method"` |
| 92 | } |
| 93 | |
| 94 | // SessionInboxCapability advertises durable inbox methods (schemaVersion 1). |
| 95 | type SessionInboxCapability struct { |
| 96 | SchemaVersion int `json:"schemaVersion"` |
| 97 | Methods map[string]string `json:"methods"` |
| 98 | } |
| 99 | |
| 100 | // SessionReloadExtensionsCapability identifies the vendor-namespaced runtime |
| 101 | // reload method. |
| 102 | type SessionReloadExtensionsCapability struct { |
| 103 | Method string `json:"method"` |
| 104 | } |
| 105 | |
| 106 | const ( |
| 107 | // reasonixExtensionSurfaceSchemaVersion versions the extension-surface DTO |
| 108 | // carried by the vendor session/update variant. |
| 109 | reasonixExtensionSurfaceSchemaVersion = 1 |
| 110 | // extensionSurfaceUpdateKind discriminates the vendor session/update |
| 111 | // variant that carries a structured extension-UI surface. |
| 112 | extensionSurfaceUpdateKind = "_reasonix.io/extension_surface" |
| 113 | ) |
| 114 | |
| 115 | // ExtensionSurfaceCapability advertises that a participant renders structured |
| 116 | // extension-UI surfaces (Extension Protocol v2) natively. |
| 117 | type ExtensionSurfaceCapability struct { |
| 118 | Supported bool `json:"supported"` |
| 119 | SchemaVersion int `json:"schemaVersion"` |
| 120 | } |
| 121 | |
| 122 | // EmptyCapability serializes to {} for ACP capability flags. |
| 123 | type EmptyCapability struct{} |
| 124 | |
| 125 | // SessionCapabilities advertises optional session lifecycle methods. |
| 126 | type SessionCapabilities struct { |
| 127 | List *EmptyCapability `json:"list,omitempty"` |
| 128 | Resume *EmptyCapability `json:"resume,omitempty"` |
| 129 | Close *EmptyCapability `json:"close,omitempty"` |
| 130 | Delete *EmptyCapability `json:"delete,omitempty"` |
| 131 | } |
| 132 | |
| 133 | // PromptCapabilities reports which content-block kinds prompts may carry. |
| 134 | type PromptCapabilities struct { |
| 135 | Image bool `json:"image"` |
| 136 | Audio bool `json:"audio"` |
| 137 | EmbeddedContext bool `json:"embeddedContext"` |
| 138 | } |
| 139 | |
| 140 | // MCPCapabilities reports which MCP transports session/new accepts. |
| 141 | type MCPCapabilities struct { |
| 142 | HTTP bool `json:"http"` |
| 143 | SSE bool `json:"sse"` |
| 144 | } |
| 145 | |
| 146 | // AuthMethod advertises how a client can prepare credentials for the agent. |
| 147 | type AuthMethod struct { |
| 148 | ID string `json:"id"` |
| 149 | Name string `json:"name"` |
| 150 | Description string `json:"description,omitempty"` |
| 151 | Type string `json:"type,omitempty"` |
| 152 | Args []string `json:"args,omitempty"` |
| 153 | Env map[string]string `json:"env,omitempty"` |
| 154 | } |
| 155 | |
| 156 | // AuthenticateParams selects one advertised auth method. Terminal methods are |
| 157 | // normally handled by the client by launching the agent with the method's args; |
| 158 | // accepting this request keeps clients that call authenticate directly working. |
| 159 | type AuthenticateParams struct { |
| 160 | MethodID string `json:"methodId"` |
| 161 | } |
| 162 | |
| 163 | // AuthenticateResult is the empty authentication ack. |
| 164 | type AuthenticateResult struct{} |
| 165 | |
| 166 | // session/new |
| 167 | |
| 168 | // SessionNewParams opens a session rooted at cwd, optionally with MCP servers |
| 169 | // the agent should connect for the session's lifetime. |
| 170 | type SessionNewParams struct { |
| 171 | Cwd string `json:"cwd,omitempty"` |
| 172 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 173 | } |
| 174 | |
| 175 | // MCPServerSpec describes one MCP server the client asks the agent to connect. |
| 176 | type MCPServerSpec struct { |
| 177 | Name string `json:"name"` |
| 178 | Type string `json:"type,omitempty"` |
| 179 | Command string `json:"command,omitempty"` |
| 180 | Args []string `json:"args,omitempty"` |
| 181 | Env MCPEnv `json:"env,omitempty"` |
| 182 | URL string `json:"url,omitempty"` |
| 183 | Headers MCPHeaders `json:"headers,omitempty"` |
| 184 | } |
| 185 | |
| 186 | // MCPEnv accepts ACP's official EnvVariable[] shape while still accepting the |
| 187 | // older map shape that Reasonix v1 clients used. |
| 188 | type MCPEnv map[string]string |
| 189 | |
| 190 | // MCPHeaders accepts ACP's official HTTPHeader[] shape while still accepting |
| 191 | // the older map shape that Reasonix v1 clients used. The official spec |
| 192 | // (https://agentclientprotocol.com) ships HTTP/SSE MCP headers as an array of |
| 193 | // {name,value} objects, even when empty. |
| 194 | type MCPHeaders map[string]string |
| 195 | |
| 196 | // EnvVariable is one official ACP MCP environment variable entry. The same |
| 197 | // {name,value} shape is also used by HTTP/SSE headers in the ACP spec, so we |
| 198 | // reuse it as the parse target for [MCPHeaders] too. |
| 199 | type EnvVariable struct { |
| 200 | Name string `json:"name"` |
| 201 | Value string `json:"value"` |
| 202 | } |
| 203 | |
| 204 | func (e *MCPEnv) UnmarshalJSON(raw []byte) error { |
| 205 | out, err := unmarshalNameValueMap(raw, "env") |
| 206 | if err != nil { |
| 207 | return err |
| 208 | } |
| 209 | *e = out |
| 210 | return nil |
| 211 | } |
| 212 | |
| 213 | func (h *MCPHeaders) UnmarshalJSON(raw []byte) error { |
| 214 | out, err := unmarshalNameValueMap(raw, "headers") |
| 215 | if err != nil { |
| 216 | return err |
| 217 | } |
| 218 | *h = out |
| 219 | return nil |
| 220 | } |
| 221 | |
| 222 | // unmarshalNameValueMap parses ACP's official [{name,value}, ...] array shape |
| 223 | // or the legacy {name: value, ...} map shape into a map. field names the JSON |
| 224 | // field for error messages. |
| 225 | func unmarshalNameValueMap(raw []byte, field string) (map[string]string, error) { |
| 226 | if s := strings.TrimSpace(string(raw)); s == "" || s == "null" { |
| 227 | return nil, nil |
| 228 | } |
| 229 | |
| 230 | var vars []EnvVariable |
| 231 | if err := json.Unmarshal(raw, &vars); err == nil { |
| 232 | out := make(map[string]string, len(vars)) |
| 233 | for i, v := range vars { |
| 234 | if strings.TrimSpace(v.Name) == "" { |
| 235 | return nil, fmt.Errorf("%s[%d].name is required", field, i) |
| 236 | } |
| 237 | out[v.Name] = v.Value |
| 238 | } |
| 239 | return out, nil |
| 240 | } |
| 241 | |
| 242 | var legacy map[string]string |
| 243 | if err := json.Unmarshal(raw, &legacy); err == nil { |
| 244 | return legacy, nil |
| 245 | } |
| 246 | return nil, fmt.Errorf("%s must be an array of {name,value} objects", field) |
| 247 | } |
| 248 | |
| 249 | // SessionNewResult returns the opaque id used to address the session thereafter. |
| 250 | type SessionNewResult struct { |
| 251 | SessionID string `json:"sessionId"` |
| 252 | Models *SessionModelState `json:"models,omitempty"` |
| 253 | Modes *SessionModeState `json:"modes,omitempty"` |
| 254 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 255 | } |
| 256 | |
| 257 | // session modes |
| 258 | |
| 259 | // SessionMode is one operating mode the client can switch the session into. |
| 260 | type SessionMode struct { |
| 261 | ID string `json:"id"` |
| 262 | Name string `json:"name"` |
| 263 | Description string `json:"description,omitempty"` |
| 264 | } |
| 265 | |
| 266 | // SessionModeState reports the current mode and the full mode list. |
| 267 | type SessionModeState struct { |
| 268 | CurrentModeID string `json:"currentModeId"` |
| 269 | AvailableModes []SessionMode `json:"availableModes"` |
| 270 | } |
| 271 | |
| 272 | // SessionSetModeParams switches a session's operating mode. |
| 273 | type SessionSetModeParams struct { |
| 274 | SessionID string `json:"sessionId"` |
| 275 | ModeID string `json:"modeId"` |
| 276 | } |
| 277 | |
| 278 | // SessionSetModeResult is the empty ack. |
| 279 | type SessionSetModeResult struct{} |
| 280 | |
| 281 | // ModelInfo describes one selectable model in ACP's legacy model selector. |
| 282 | type ModelInfo struct { |
| 283 | ModelID string `json:"modelId"` |
| 284 | Name string `json:"name"` |
| 285 | Description string `json:"description,omitempty"` |
| 286 | } |
| 287 | |
| 288 | // SessionModelState is ACP's legacy model selector state. New clients should |
| 289 | // prefer the category:"model" config option, but some hosts still probe this. |
| 290 | type SessionModelState struct { |
| 291 | AvailableModels []ModelInfo `json:"availableModels"` |
| 292 | CurrentModelID string `json:"currentModelId"` |
| 293 | } |
| 294 | |
| 295 | // session/load |
| 296 | |
| 297 | // SessionLoadParams resumes a session saved under sessionId (the id a prior |
| 298 | // session/new returned), optionally re-rooting it at cwd with fresh MCP servers. |
| 299 | // The agent replays the stored conversation as session/update notifications |
| 300 | // before the request returns. |
| 301 | type SessionLoadParams struct { |
| 302 | SessionID string `json:"sessionId"` |
| 303 | Cwd string `json:"cwd,omitempty"` |
| 304 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 305 | } |
| 306 | |
| 307 | // SessionLoadResult is the empty ack; the conversation has already arrived as a |
| 308 | // burst of session/update notifications by the time it is sent. |
| 309 | type SessionLoadResult struct { |
| 310 | Models *SessionModelState `json:"models,omitempty"` |
| 311 | Modes *SessionModeState `json:"modes,omitempty"` |
| 312 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 313 | } |
| 314 | |
| 315 | // session/resume |
| 316 | |
| 317 | // SessionResumeParams resumes a session without replaying its transcript. |
| 318 | type SessionResumeParams struct { |
| 319 | SessionID string `json:"sessionId"` |
| 320 | Cwd string `json:"cwd,omitempty"` |
| 321 | MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` |
| 322 | } |
| 323 | |
| 324 | // SessionResumeResult is the empty ack returned once the session is ready. |
| 325 | type SessionResumeResult struct { |
| 326 | Models *SessionModelState `json:"models,omitempty"` |
| 327 | Modes *SessionModeState `json:"modes,omitempty"` |
| 328 | ConfigOptions []SessionConfigOption `json:"configOptions,omitempty"` |
| 329 | } |
| 330 | |
| 331 | // session/set_config_option |
| 332 | |
| 333 | // SetSessionConfigOptionParams changes one advertised session config option. |
| 334 | type SetSessionConfigOptionParams struct { |
| 335 | SessionID string `json:"sessionId"` |
| 336 | ConfigID string `json:"configId"` |
| 337 | Value string `json:"value"` |
| 338 | } |
| 339 | |
| 340 | type SetSessionConfigOptionResult struct { |
| 341 | ConfigOptions []SessionConfigOption `json:"configOptions"` |
| 342 | DeprecatedNotice string `json:"deprecatedNotice,omitempty"` |
| 343 | } |
| 344 | |
| 345 | // SessionConfigOption is a single-value ACP session selector. |
| 346 | type SessionConfigOption struct { |
| 347 | ID string `json:"id"` |
| 348 | Name string `json:"name"` |
| 349 | Description string `json:"description,omitempty"` |
| 350 | Category string `json:"category,omitempty"` |
| 351 | Type string `json:"type"` |
| 352 | CurrentValue string `json:"currentValue"` |
| 353 | Options []SessionConfigSelectOption `json:"options"` |
| 354 | } |
| 355 | |
| 356 | // SessionConfigSelectOption is one selectable value for a config option. |
| 357 | type SessionConfigSelectOption struct { |
| 358 | Value string `json:"value"` |
| 359 | Name string `json:"name"` |
| 360 | Description string `json:"description,omitempty"` |
| 361 | } |
| 362 | |
| 363 | // session/set_model |
| 364 | |
| 365 | // SetSessionModelParams is ACP's legacy model-switching request. |
| 366 | type SetSessionModelParams struct { |
| 367 | SessionID string `json:"sessionId"` |
| 368 | ModelID string `json:"modelId"` |
| 369 | } |
| 370 | |
| 371 | // SetSessionModelResult is the empty ack for legacy model switching. |
| 372 | type SetSessionModelResult struct{} |
| 373 | |
| 374 | // session/list |
| 375 | |
| 376 | // SessionListParams lists known sessions, optionally filtered by cwd. |
| 377 | type SessionListParams struct { |
| 378 | Cwd string `json:"cwd,omitempty"` |
| 379 | Cursor string `json:"cursor,omitempty"` |
| 380 | } |
| 381 | |
| 382 | // SessionListResult is the first and only page of sessions Reasonix currently |
| 383 | // returns. NextCursor is omitted because the in-process list is unpaged. |
| 384 | type SessionListResult struct { |
| 385 | Sessions []SessionInfo `json:"sessions"` |
| 386 | NextCursor string `json:"nextCursor,omitempty"` |
| 387 | } |
| 388 | |
| 389 | // SessionInfo is the ACP session/list item shape. |
| 390 | type SessionInfo struct { |
| 391 | SessionID string `json:"sessionId"` |
| 392 | Cwd string `json:"cwd"` |
| 393 | Title string `json:"title,omitempty"` |
| 394 | UpdatedAt string `json:"updatedAt,omitempty"` |
| 395 | Meta map[string]any `json:"_meta,omitempty"` |
| 396 | } |
| 397 | |
| 398 | // session/close |
| 399 | |
| 400 | // SessionCloseParams closes an active session and releases its resources. |
| 401 | type SessionCloseParams struct { |
| 402 | SessionID string `json:"sessionId"` |
| 403 | } |
| 404 | |
| 405 | // SessionCloseResult is the empty close ack. |
| 406 | type SessionCloseResult struct{} |
| 407 | |
| 408 | // session/delete |
| 409 | |
| 410 | // SessionDeleteParams removes a session from future session/list results. |
| 411 | type SessionDeleteParams struct { |
| 412 | SessionID string `json:"sessionId"` |
| 413 | } |
| 414 | |
| 415 | // SessionDeleteResult is the empty delete ack. |
| 416 | type SessionDeleteResult struct{} |
| 417 | |
| 418 | // content blocks (inbound prompt) |
| 419 | |
| 420 | // ContentBlock is one piece of a prompt. The agent reads text blocks and the |
| 421 | // inline text of resource blocks (embeddedContext); image/audio are accepted on |
| 422 | // the wire but ignored, matching the advertised capabilities. |
| 423 | type ContentBlock struct { |
| 424 | Type string `json:"type"` |
| 425 | Text string `json:"text,omitempty"` |
| 426 | Resource *ResourceContents `json:"resource,omitempty"` |
| 427 | MimeType string `json:"mimeType,omitempty"` |
| 428 | Data string `json:"data,omitempty"` |
| 429 | } |
| 430 | |
| 431 | // ResourceContents is the embedded resource of a "resource" content block. |
| 432 | type ResourceContents struct { |
| 433 | URI string `json:"uri"` |
| 434 | MimeType string `json:"mimeType,omitempty"` |
| 435 | Text string `json:"text,omitempty"` |
| 436 | } |
| 437 | |
| 438 | // FlattenPrompt extracts the user-visible prompt text out of ACP content blocks. |
| 439 | // Text blocks contribute their text; resource blocks contribute their inline |
| 440 | // text when present (embeddedContext). Other block kinds are dropped. Ported from |
| 441 | // protocol.ts flattenPrompt. |
| 442 | func FlattenPrompt(blocks []ContentBlock) string { |
| 443 | parts := make([]string, 0, len(blocks)) |
| 444 | for _, b := range blocks { |
| 445 | switch b.Type { |
| 446 | case "text": |
| 447 | if b.Text != "" { |
| 448 | parts = append(parts, b.Text) |
| 449 | } |
| 450 | case "resource": |
| 451 | if b.Resource != nil && b.Resource.Text != "" { |
| 452 | parts = append(parts, b.Resource.Text) |
| 453 | } |
| 454 | } |
| 455 | } |
| 456 | return strings.TrimSpace(strings.Join(parts, "\n\n")) |
| 457 | } |
| 458 | |
| 459 | // session/prompt |
| 460 | |
| 461 | // SessionPromptParams sends a turn's prompt to a session. |
| 462 | type SessionPromptParams struct { |
| 463 | SessionID string `json:"sessionId"` |
| 464 | Prompt []ContentBlock `json:"prompt"` |
| 465 | // Action is an optional Reasonix extension. Empty preserves ACP's standard |
| 466 | // prompt behavior; final_readiness_recovery explicitly resumes the newest |
| 467 | // paused host check without trusting ordinary prose as authorization. |
| 468 | Action string `json:"action,omitempty"` |
| 469 | RecoveryID string `json:"recoveryId,omitempty"` |
| 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 durable steer admission. |
| 480 | type SessionSteerResult struct { |
| 481 | ItemID string `json:"itemId,omitempty"` |
| 482 | Disposition string `json:"disposition,omitempty"` |
| 483 | } |
| 484 | |
| 485 | // sessionSteerMethod follows ACP v1's reserved vendor-extension namespace. |
| 486 | const sessionSteerMethod = "_reasonix.io/session/steer" |
| 487 | |
| 488 | const ( |
| 489 | sessionInboxSchemaVersion = 1 |
| 490 | sessionInboxEnqueueMethod = "_reasonix.io/session/inbox/enqueue" |
| 491 | sessionInboxListMethod = "_reasonix.io/session/inbox/list" |
| 492 | sessionInboxGetMethod = "_reasonix.io/session/inbox/get" |
| 493 | sessionInboxUpdateMethod = "_reasonix.io/session/inbox/update" |
| 494 | sessionInboxDeleteMethod = "_reasonix.io/session/inbox/delete" |
| 495 | sessionInboxMoveMethod = "_reasonix.io/session/inbox/move" |
| 496 | sessionInboxPauseMethod = "_reasonix.io/session/inbox/setPaused" |
| 497 | sessionInboxRetryMethod = "_reasonix.io/session/inbox/retry" |
| 498 | sessionInboxRefreshMethod = "_reasonix.io/session/inbox/refresh" |
| 499 | ) |
| 500 | |
| 501 | // SessionInboxEnqueueParams is the durable inbox enqueue request. |
| 502 | type SessionInboxEnqueueParams struct { |
| 503 | SessionID string `json:"sessionId"` |
| 504 | Text string `json:"text"` |
| 505 | Intent string `json:"intent,omitempty"` // followup | steer |
| 506 | IdempotencyKey string `json:"idempotencyKey,omitempty"` |
| 507 | } |
| 508 | |
| 509 | // SessionInboxItemParams identifies one inbox item. |
| 510 | type SessionInboxItemParams struct { |
| 511 | SessionID string `json:"sessionId"` |
| 512 | ItemID string `json:"itemId"` |
| 513 | } |
| 514 | |
| 515 | // SessionInboxUpdateParams rewrites an item body. |
| 516 | type SessionInboxUpdateParams struct { |
| 517 | SessionID string `json:"sessionId"` |
| 518 | ItemID string `json:"itemId"` |
| 519 | Text string `json:"text"` |
| 520 | } |
| 521 | |
| 522 | // SessionInboxMoveParams reorders an item (toIndex is 0-based). |
| 523 | type SessionInboxMoveParams struct { |
| 524 | SessionID string `json:"sessionId"` |
| 525 | ItemID string `json:"itemId"` |
| 526 | ToIndex int `json:"toIndex"` |
| 527 | } |
| 528 | |
| 529 | // SessionInboxPauseParams toggles pause. |
| 530 | type SessionInboxPauseParams struct { |
| 531 | SessionID string `json:"sessionId"` |
| 532 | Paused bool `json:"paused"` |
| 533 | } |
| 534 | |
| 535 | // SessionReloadExtensionsParams addresses one live ACP session. |
| 536 | type SessionReloadExtensionsParams struct { |
| 537 | SessionID string `json:"sessionId"` |
| 538 | } |
| 539 | |
| 540 | // SessionReloadExtensionsResult reports whether the runtime reload ran |
| 541 | // immediately (Queued false) or was coalesced behind a turn/rebuild in flight |
| 542 | // to run when the session goes idle (Queued true). |
| 543 | type SessionReloadExtensionsResult struct { |
| 544 | Queued bool `json:"queued,omitempty"` |
| 545 | } |
| 546 | |
| 547 | // sessionReloadExtensionsMethod follows ACP v1's reserved vendor-extension |
| 548 | // namespace, like sessionSteerMethod: only the "_<vendor>/" prefix is reserved |
| 549 | // for vendor methods, so the bare "reasonix/session/reloadExtensions" form |
| 550 | // could collide with a future official ACP method and must not be used. |
| 551 | const sessionReloadExtensionsMethod = "_reasonix.io/session/reloadExtensions" |
| 552 | |
| 553 | // StopReason tells the client why a turn ended. Reasonix only emits values from |
| 554 | // the ACP v1 enum; failed turns are returned as JSON-RPC errors instead. |
| 555 | type StopReason string |
| 556 | |
| 557 | // SessionPromptResult ends a session/prompt. TranscriptPath is reserved for a |
| 558 | // future on-disk transcript pointer; omitted (null) for now. |
| 559 | type SessionPromptResult struct { |
| 560 | StopReason StopReason `json:"stopReason"` |
| 561 | TranscriptPath *string `json:"transcriptPath,omitempty"` |
| 562 | } |
| 563 | |
| 564 | // session/update (agent → client notifications) |
| 565 | // |
| 566 | // SessionUpdate is a tagged union discriminated by sessionUpdate. The variants |
| 567 | // reuse the JSON key "content" with two incompatible shapes (a single block for |
| 568 | // message chunks, an array for tool results), so we model each variant as its own |
| 569 | // struct rather than one struct with conflicting tags, and carry it through |
| 570 | // SessionUpdateParams.Update as an interface value. |
| 571 | |
| 572 | // SessionUpdateParams wraps one update for a session. |
| 573 | type SessionUpdateParams struct { |
| 574 | SessionID string `json:"sessionId"` |
| 575 | Update any `json:"update"` |
| 576 | } |
| 577 | |
| 578 | // messageChunk is agent_message_chunk / agent_thought_chunk. |
| 579 | type messageChunk struct { |
| 580 | SessionUpdate string `json:"sessionUpdate"` |
| 581 | Content ContentBlock `json:"content"` |
| 582 | Metadata *updateMeta `json:"metadata,omitempty"` |
| 583 | } |
| 584 | |
| 585 | // extensionSurfaceUpdate is the vendor session/update variant that carries one |
| 586 | // structured extension-UI surface to clients that negotiated |
| 587 | // reasonix.extensionSurface in initialize. ACP has no standard notification for |
| 588 | // extension surfaces, so the DTO (the shared eventwire JSON contract) rides |
| 589 | // _meta["reasonix.io"]["extensionSurface"], mirroring how the initialize |
| 590 | // handshake namespaces vendor data under "reasonix.io". The sink always pairs |
| 591 | // it with a flattened agent_message_chunk text fallback (belt and suspenders): |
| 592 | // a client that ignores the vendor variant still shows the content. |
| 593 | type extensionSurfaceUpdate struct { |
| 594 | SessionUpdate string `json:"sessionUpdate"` |
| 595 | Meta map[string]any `json:"_meta"` |
| 596 | } |
| 597 | |
| 598 | // updateMeta carries optional error detail on an agent_message_chunk. |
| 599 | type updateMeta struct { |
| 600 | Error *updateError `json:"error,omitempty"` |
| 601 | } |
| 602 | |
| 603 | type updateError struct { |
| 604 | Name string `json:"name"` |
| 605 | Message string `json:"message"` |
| 606 | } |
| 607 | |
| 608 | // toolCall is a "tool_call" update (announces a call, with title/kind/rawInput). |
| 609 | type toolCall struct { |
| 610 | SessionUpdate string `json:"sessionUpdate"` |
| 611 | ToolCallID string `json:"toolCallId"` |
| 612 | Title string `json:"title,omitempty"` |
| 613 | Kind string `json:"kind,omitempty"` |
| 614 | Status string `json:"status,omitempty"` |
| 615 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 616 | Locations []ToolCallLocation `json:"locations,omitempty"` |
| 617 | } |
| 618 | |
| 619 | // ToolCallLocation names a file (and optionally a line) a tool call touches, so |
| 620 | // the client can follow along in the editor. |
| 621 | type ToolCallLocation struct { |
| 622 | Path string `json:"path"` |
| 623 | Line *int `json:"line,omitempty"` |
| 624 | } |
| 625 | |
| 626 | // toolCallUpdateMsg is a "tool_call_update" update (status + result content). |
| 627 | type toolCallUpdateMsg struct { |
| 628 | SessionUpdate string `json:"sessionUpdate"` |
| 629 | ToolCallID string `json:"toolCallId"` |
| 630 | Status string `json:"status,omitempty"` |
| 631 | Content []toolContent `json:"content,omitempty"` |
| 632 | } |
| 633 | |
| 634 | // toolContent wraps a tool result's text, per the ACP tool_call_update shape. |
| 635 | type toolContent struct { |
| 636 | Type string `json:"type"` |
| 637 | Content ContentBlock `json:"content"` |
| 638 | } |
| 639 | |
| 640 | // availableCommandsUpdate advertises slash commands that the ACP client may |
| 641 | // surface in its composer. The client sends invocations back as normal |
| 642 | // session/prompt text such as "/review diff". |
| 643 | type availableCommandsUpdate struct { |
| 644 | SessionUpdate string `json:"sessionUpdate"` |
| 645 | AvailableCommands []AvailableCommand `json:"availableCommands"` |
| 646 | } |
| 647 | |
| 648 | // AvailableCommand is one slash command available in a session. |
| 649 | type AvailableCommand struct { |
| 650 | Name string `json:"name"` |
| 651 | Description string `json:"description"` |
| 652 | Input *AvailableCommandInput `json:"input,omitempty"` |
| 653 | } |
| 654 | |
| 655 | // AvailableCommandInput describes a command's free-form text argument. |
| 656 | type AvailableCommandInput struct { |
| 657 | Hint string `json:"hint"` |
| 658 | } |
| 659 | |
| 660 | // configOptionUpdate reports a complete refreshed session config state. |
| 661 | type configOptionUpdate struct { |
| 662 | SessionUpdate string `json:"sessionUpdate"` |
| 663 | ConfigOptions []SessionConfigOption `json:"configOptions"` |
| 664 | } |
| 665 | |
| 666 | // planUpdate is a "plan" update: the agent's current task list. Each update |
| 667 | // carries the complete plan and replaces the previous one, mirroring the |
| 668 | // todo_write contract it is derived from. |
| 669 | type planUpdate struct { |
| 670 | SessionUpdate string `json:"sessionUpdate"` |
| 671 | Entries []PlanEntry `json:"entries"` |
| 672 | } |
| 673 | |
| 674 | // PlanEntry is one task in a plan update. |
| 675 | type PlanEntry struct { |
| 676 | Content string `json:"content"` |
| 677 | Priority string `json:"priority"` |
| 678 | Status string `json:"status"` |
| 679 | } |
| 680 | |
| 681 | // currentModeUpdate reports that the session switched operating modes. |
| 682 | type currentModeUpdate struct { |
| 683 | SessionUpdate string `json:"sessionUpdate"` |
| 684 | CurrentModeID string `json:"currentModeId"` |
| 685 | } |
| 686 | |
| 687 | // fs/* (agent → client requests) |
| 688 | |
| 689 | // FSReadTextFileParams asks the client for a file's current text, including |
| 690 | // unsaved editor state. Line (1-based) and Limit page the content; Reasonix |
| 691 | // always reads whole files and pages locally, so it sends neither. |
| 692 | type FSReadTextFileParams struct { |
| 693 | SessionID string `json:"sessionId"` |
| 694 | Path string `json:"path"` |
| 695 | Line *int `json:"line,omitempty"` |
| 696 | Limit *int `json:"limit,omitempty"` |
| 697 | } |
| 698 | |
| 699 | // FSReadTextFileResult carries the file content. |
| 700 | type FSReadTextFileResult struct { |
| 701 | Content string `json:"content"` |
| 702 | } |
| 703 | |
| 704 | // FSWriteTextFileParams asks the client to write content to path, updating any |
| 705 | // open buffer as well as the file on disk. |
| 706 | type FSWriteTextFileParams struct { |
| 707 | SessionID string `json:"sessionId"` |
| 708 | Path string `json:"path"` |
| 709 | Content string `json:"content"` |
| 710 | } |
| 711 | |
| 712 | // terminal/* (agent → client requests) |
| 713 | |
| 714 | // TerminalCreateParams starts a command in a client-owned terminal. |
| 715 | // Env follows ACP v1's official EnvVariable[] shape (same as MCP env): only |
| 716 | // the overrides Reasonix owns (typically TMPDIR/TMP/TEMP) are sent — never a |
| 717 | // full host environment dump. |
| 718 | type TerminalCreateParams struct { |
| 719 | SessionID string `json:"sessionId"` |
| 720 | Command string `json:"command"` |
| 721 | Args []string `json:"args,omitempty"` |
| 722 | Cwd string `json:"cwd,omitempty"` |
| 723 | Env []EnvVariable `json:"env,omitempty"` |
| 724 | OutputByteLimit int `json:"outputByteLimit,omitempty"` |
| 725 | } |
| 726 | |
| 727 | // TerminalCreateResult returns the id used by the other terminal methods. |
| 728 | type TerminalCreateResult struct { |
| 729 | TerminalID string `json:"terminalId"` |
| 730 | } |
| 731 | |
| 732 | // TerminalIDParams addresses one terminal (output / kill / wait / release). |
| 733 | type TerminalIDParams struct { |
| 734 | SessionID string `json:"sessionId"` |
| 735 | TerminalID string `json:"terminalId"` |
| 736 | } |
| 737 | |
| 738 | // TerminalOutputResult is the terminal's captured output so far. |
| 739 | type TerminalOutputResult struct { |
| 740 | Output string `json:"output"` |
| 741 | Truncated bool `json:"truncated"` |
| 742 | ExitStatus *TerminalExitStatus `json:"exitStatus,omitempty"` |
| 743 | } |
| 744 | |
| 745 | // TerminalWaitResult reports how the command exited. |
| 746 | type TerminalWaitResult struct { |
| 747 | ExitCode *int `json:"exitCode,omitempty"` |
| 748 | Signal *string `json:"signal,omitempty"` |
| 749 | } |
| 750 | |
| 751 | // TerminalExitStatus mirrors TerminalWaitResult inside terminal/output. |
| 752 | type TerminalExitStatus struct { |
| 753 | ExitCode *int `json:"exitCode,omitempty"` |
| 754 | Signal *string `json:"signal,omitempty"` |
| 755 | } |
| 756 | |
| 757 | // session/cancel (client → agent notification) |
| 758 | |
| 759 | // SessionCancelParams cancels an in-progress turn. |
| 760 | type SessionCancelParams struct { |
| 761 | SessionID string `json:"sessionId"` |
| 762 | } |
| 763 | |
| 764 | // session/request_permission (agent → client request) |
| 765 | |
| 766 | // PermissionOptionKind classifies an option for host UI styling. It is an ACP v1 |
| 767 | // wire enum, so host-visible permission choices must stay within the official |
| 768 | // protocol values. |
| 769 | type PermissionOptionKind string |
| 770 | |
| 771 | const ( |
| 772 | OptAllowOnce PermissionOptionKind = "allow_once" |
| 773 | OptAllowAlways PermissionOptionKind = "allow_always" |
| 774 | OptRejectOnce PermissionOptionKind = "reject_once" |
| 775 | OptRejectAlways PermissionOptionKind = "reject_always" |
| 776 | ) |
| 777 | |
| 778 | // PermissionOption is one choice offered to the user for a permission request. |
| 779 | type PermissionOption struct { |
| 780 | OptionID string `json:"optionId"` |
| 781 | Name string `json:"name"` |
| 782 | Kind PermissionOptionKind `json:"kind"` |
| 783 | } |
| 784 | |
| 785 | // PermissionRequestParams asks the client to approve a pending tool call. |
| 786 | type PermissionRequestParams struct { |
| 787 | SessionID string `json:"sessionId"` |
| 788 | ToolCall PermissionToolCall `json:"toolCall"` |
| 789 | Options []PermissionOption `json:"options"` |
| 790 | } |
| 791 | |
| 792 | // PermissionToolCall describes the call awaiting approval. |
| 793 | type PermissionToolCall struct { |
| 794 | ToolCallID string `json:"toolCallId"` |
| 795 | Title string `json:"title,omitempty"` |
| 796 | Kind string `json:"kind,omitempty"` |
| 797 | Status string `json:"status,omitempty"` |
| 798 | Content []toolContent `json:"content,omitempty"` |
| 799 | RawInput json.RawMessage `json:"rawInput,omitempty"` |
| 800 | Locations []ToolCallLocation `json:"locations,omitempty"` |
| 801 | Meta map[string]any `json:"_meta,omitempty"` |
| 802 | } |
| 803 | |
| 804 | // PermissionRequestResult is the client's reply to a permission request. |
| 805 | type PermissionRequestResult struct { |
| 806 | Outcome PermissionOutcome `json:"outcome"` |
| 807 | } |
| 808 | |
| 809 | // PermissionOutcome is "selected" (with optionId) or "cancelled". |
| 810 | type PermissionOutcome struct { |
| 811 | Outcome string `json:"outcome"` |
| 812 | OptionID string `json:"optionId,omitempty"` |
| 813 | } |
| 814 |