forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | package bridge |
| 2 | ||
| 3 | import "encoding/json" | |
| 4 | ||
| 5 | // This file is the wire contract between the SPA and the backend, over a | |
| 6 | // WebSocket carrying one JSON message per text frame. ACP payloads (session | |
| 7 | // updates, permission requests) are relayed verbatim so the frontend works | |
| 8 | // with the protocol's own shapes, documented at | |
| 9 | // https://agentclientprotocol.com. | |
| 10 | ||
| 11 | // Incoming message types (browser → server). | |
| 12 | const ( | |
| 13 | // IncomingPrompt starts a turn: {"type":"prompt","text":"..."}. | |
| 14 | IncomingPrompt = "prompt" | |
| 15 | // IncomingCancel interrupts the current turn: {"type":"cancel"}. | |
| 16 | IncomingCancel = "cancel" | |
| 17 | // IncomingPermissionResponse answers a permission request: | |
| 18 | // {"type":"permission_response","requestId":"...","optionId":"..."} or | |
| 19 | // {"type":"permission_response","requestId":"...","cancelled":true}. | |
| 20 | IncomingPermissionResponse = "permission_response" | |
| 21 | ) | |
| 22 | ||
| 23 | // Outgoing message types (server → browser). | |
| 24 | const ( | |
| 25 | // OutgoingHello is sent once per connection with the session state. | |
| 26 | OutgoingHello = "hello" | |
| 27 | // OutgoingUserMessage echoes the prompt that started a turn, so every | |
| 28 | // client (and every reconnection replay) renders the user's side of the | |
| 29 | // conversation from the same source of truth as the agent's side. | |
| 30 | OutgoingUserMessage = "user_message" | |
| 31 | // OutgoingSessionUpdate relays one ACP session/update notification; | |
| 32 | // its "update" field is the raw ACP SessionUpdate object. | |
| 33 | OutgoingSessionUpdate = "session_update" | |
| 34 | // OutgoingPermissionRequest relays an ACP permission request; its | |
| 35 | // "request" field is the raw ACP RequestPermissionRequest object. | |
| 36 | OutgoingPermissionRequest = "permission_request" | |
| 37 | // OutgoingPermissionResolved tells every client a permission request | |
| 38 | // was answered (so open dialogs close everywhere). | |
| 39 | OutgoingPermissionResolved = "permission_resolved" | |
| 40 | // OutgoingTurnStarted marks the start of a prompt turn. | |
| 41 | OutgoingTurnStarted = "turn_started" | |
| 42 | // OutgoingTurnEnded marks the end of a turn, with its stop reason. | |
| 43 | OutgoingTurnEnded = "turn_ended" | |
| 44 | // OutgoingError carries a human-readable error message. | |
| 45 | OutgoingError = "error" | |
| 46 | ) | |
| 47 | ||
| 48 | // Incoming is a message sent by the browser. | |
| 49 | type Incoming struct { | |
| 50 | Type string `json:"type"` | |
| 51 | // Text is the prompt text (IncomingPrompt). | |
| 52 | Text string `json:"text,omitempty"` | |
| 53 | // RequestId identifies the permission request being answered. | |
| 54 | RequestId string `json:"requestId,omitempty"` | |
| 55 | // OptionId is the chosen ACP permission option. | |
| 56 | OptionId string `json:"optionId,omitempty"` | |
| 57 | // Cancelled dismisses the permission request instead of choosing. | |
| 58 | Cancelled bool `json:"cancelled,omitempty"` | |
| 59 | } | |
| 60 | ||
| 61 | // Outgoing is a message sent to the browser. | |
| 62 | type Outgoing struct { | |
| 63 | Type string `json:"type"` | |
| 64 | // SessionId is set on OutgoingHello. | |
| 65 | SessionId string `json:"sessionId,omitempty"` | |
| 66 | // TurnActive is set on OutgoingHello: true while a prompt turn runs. | |
| 67 | TurnActive bool `json:"turnActive,omitempty"` | |
| 68 | // Update is the raw ACP SessionUpdate (OutgoingSessionUpdate). | |
| 69 | Update json.RawMessage `json:"update,omitempty"` | |
| 70 | // RequestId identifies a permission request across messages. | |
| 71 | RequestId string `json:"requestId,omitempty"` | |
| 72 | // Request is the raw ACP RequestPermissionRequest (OutgoingPermissionRequest). | |
| 73 | Request json.RawMessage `json:"request,omitempty"` | |
| 74 | // Text is the user's prompt text (OutgoingUserMessage). | |
| 75 | Text string `json:"text,omitempty"` | |
| 76 | // StopReason is the ACP stop reason (OutgoingTurnEnded). | |
| 77 | StopReason string `json:"stopReason,omitempty"` | |
| 78 | // Message is a human-readable error (OutgoingError). | |
| 79 | Message string `json:"message,omitempty"` | |
| 80 | } | |
| 81 | ||
| 82 | // encode marshals an Outgoing message; the bridge treats a marshalling | |
| 83 | // failure as a programming error and panics, because every field is | |
| 84 | // marshal-safe by construction. | |
| 85 | func encode(msg Outgoing) []byte { | |
| 86 | raw, err := json.Marshal(msg) | |
| 87 | if err != nil { | |
| 88 | panic(err) | |
| 89 | } | |
| 90 | return raw | |
| 91 | } |