-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Goal: Replace goosed's custom SSE-based streaming API with a standards-based ACP (Agent Communication Protocol) over HTTP interface, enabling better client portability, protocol standardization, and eventually a unified interface for all goose clients.
Background
Current Architecture
- goose-cli (Rust): Direct in-process agent communication
- goosed (goose-server): Custom REST + SSE streaming API consumed by the Electron desktop app
Proposed Architecture
A new ACP-over-HTTP server that:
- Implements the standardized ACP (Agent Communication Protocol) specification
- Uses JSON-RPC 2.0 over HTTP with SSE streaming as the transport
- Enables any ACP-compatible client to interact with goose
Prototype
See the prototype branch: https://github.com/block/goose/tree/alexhancock/goosed-acp-and-new-cli
Example of single agent (video): ref
Example of multi-agent orchestration (screenshots): ref
Phase 1: Stabilize ACP Server
Goal: Production-ready ACP server that can fully replace goosed
Tasks
- Session persistence and resumption (
session/load) - Enough goosed features to power alternative alpha CLI (extensions, modalities, streaming, etc)
- Integration tests
Success Criteria
- Core goosed features available via ACP-over-HTTP
- Integration tests passing
Phase 2: TypeScript TUI Alpha
Goal: Feature-complete TUI which will be an evolution of the current CLI
Tasks
- Equivalent of
goose configure - Equivalent of
goose session(with list, resume, etc) - Make MCP features work (sampling, elicitation, roots, etc)
- UX polish (syntax highlighting, markdown rendering, diffs, themes?)
- Build process that makes a single binary
Success Criteria
- TUI usable as daily driver
- Docs
Phase 3: Desktop Migration
Goal: Migrate Electron desktop app from goosed to ACP
Tasks
- Integrate ACP client into desktop app
- Feature flag to toggle between
goosedandgoose-acpbackends
Success Criteria
- Desktop app multi-chat works with ACP backend
- Feature flagged
- No user-facing regressions
Phase 4: Consolidation
Goal: ACP becomes the single interface for all goose clients, and new tui is fully launched
Tasks
- Deprecate and remove
goosed - Deprecate and remove
goose-cli - Update all documentation
Success Criteria
goosedremoved from codebasegoose-cliremoved from codebase- Distribute binary for new tui
- Single unified architecture for addressing goose
Technical Details
ACP Protocol Overview
The ACP protocol uses JSON-RPC 2.0 with the following methods:
Client → Server Requests (client sends, server responds):
initialize - Handshake and capability negotiation
authenticate - Authentication (currently no-op)
session/new - Create a new conversation session (returns ACP session_id)
session/load - Resume an existing session (replays history via notifications)
session/prompt - Send user message, receive streaming response
Client → Server Notifications (client sends, no response):
session/cancel - Cancel in-progress prompt
Server → Client Requests (server sends, client must respond):
request_permission - Request user confirmation for tool execution
Server → Client Notifications (server sends via SSE, no response):
SessionNotification with SessionUpdate variants:
- AgentMessageChunk - Streaming text from agent
- AgentThoughtChunk - Streaming reasoning/thinking content
- UserMessageChunk - User message (used in session/load replay)
- ToolCall - Tool invocation started (status: pending)
- ToolCallUpdate - Tool status change (completed/failed) with result
HTTP Transport
The HTTP transport wraps ACP's JSON-RPC protocol over Stremable HTTP
Message Flow
Client Server
│ │
│ ═══ Session Initialization ═══ │
│ │
│─── POST /acp ─────────────────────>│ { method: "initialize", id: 1 }
│ Accept: application/json, │ (no Acp-Session-Id header)
│ text/event-stream │
│ ┌─────────────────────│ Server creates session, opens SSE stream
│ │ (SSE stream open) │
│<─────────────│─ SSE event ─────────│ { id: 1, result: { capabilities } }
│ │ │ Response includes Acp-Session-Id header
│ ▼ │
│ │
│ ═══ Prompt Flow ═══ │
│ │
│─── POST /acp ─────────────────────>│ { method: "session/new", id: 2,
│ Acp-Session-Id: <session_id> │ params: { cwd, mcp_servers } }
│ ┌─────────────────────│ Opens new SSE stream for response
│<─────────────│─ SSE event ─────────│ { id: 2, result: { session_id: <goose_session> } }
│ ▼ │
│ │
│─── POST /acp ─────────────────────>│ { method: "session/prompt", id: 3,
│ Acp-Session-Id: <session_id> │ params: { session_id, prompt } }
│ ┌─────────────────────│ Opens new SSE stream for response
│<─────────────│─ SSE event ─────────│ notification: AgentMessageChunk
│<─────────────│─ SSE event ─────────│ notification: AgentThoughtChunk (if reasoning)
│<─────────────│─ SSE event ─────────│ notification: ToolCall (status: pending)
│<─────────────│─ SSE event ─────────│ notification: ToolCallUpdate (status: completed)
│<─────────────│─ SSE event ─────────│ notification: AgentMessageChunk
│<─────────────│─ SSE event ─────────│ { id: 3, result: { stop_reason: "end_turn" } }
│ ▼ │
│ │
│ ═══ Permission Flow ═══ │
│ (when tool requires confirmation) │
│ │
│─── POST /acp ─────────────────────>│ { method: "session/prompt", id: 4, ... }
│ Acp-Session-Id: <session_id> │
│ ┌─────────────────────│
│<─────────────│─ SSE event ─────────│ notification: ToolCall (status: pending)
│<─────────────│─ SSE event ─────────│ { method: "request_permission", id: 99, params: {...} }
│ │ │ (server-to-client request)
│ │ │
│─── POST /acp ┼────────────────────>│ { id: 99, result: { outcome: "allow_once" } }
│ Acp-Session-Id: <session_id> │ (client response, returns 202 Accepted)
│ │ │
│<─────────────│─ SSE event ─────────│ notification: ToolCallUpdate (status: completed)
│<─────────────│─ SSE event ─────────│ { id: 4, result: { stop_reason: "end_turn" } }
│ ▼ │
│ │
│ ═══ Cancel Flow ═══ │
│ │
│─── POST /acp ─────────────────────>│ { method: "session/prompt", id: 5, ... }
│ Acp-Session-Id: <session_id> │
│ ┌─────────────────────│
│<─────────────│─ SSE event ─────────│ notification: AgentMessageChunk
│ │ │
│─── POST /acp ┼────────────────────>│ { method: "session/cancel" }
│ Acp-Session-Id: <session_id> │ (notification, no id - returns 202 Accepted)
│ │ │
│<─────────────│─ SSE event ─────────│ { id: 5, result: { stop_reason: "cancelled" } }
│ ▼ │
│ │
│ ═══ Resume Session Flow ═══ │
│ │
│─── POST /acp ─────────────────────>│ { method: "initialize", id: 1 }
│ (no Acp-Session-Id) │ New HTTP session
│ ┌─────────────────────│
│<─────────────│─ SSE event ─────────│ { id: 1, result: { capabilities } }
│ │ │ Response includes new Acp-Session-Id
│ ▼ │
│ │
│─── POST /acp ─────────────────────>│ { method: "session/load", id: 2,
│ Acp-Session-Id: <new_session> │ params: { session_id: <existing_goose_session>, cwd } }
│ ┌─────────────────────│
│<─────────────│─ SSE event ─────────│ notification: UserMessageChunk (history replay)
│<─────────────│─ SSE event ─────────│ notification: AgentMessageChunk (history replay)
│<─────────────│─ SSE event ─────────│ notification: ToolCall (history replay)
│<─────────────│─ SSE event ─────────│ notification: ToolCallUpdate (history replay)
│<─────────────│─ SSE event ─────────│ { id: 2, result: {} }
│ ▼ │
│ │
│ ═══ Standalone SSE Stream ═══ │
│ (optional, for server-initiated) │
│ │
│─── GET /acp ──────────────────────>│ Open dedicated SSE listener
│ Acp-Session-Id: <session_id> │
│ Accept: text/event-stream │
│ ┌─────────────────────│ Long-lived connection for
│ │ (SSE stream open) │ server-initiated messages
│ ▼ │
│ │
│ ═══ Session Termination ═══ │
│ │
│─── DELETE /acp ───────────────────>│ Terminate session
│ Acp-Session-Id: <session_id> │
│<────────── 202 Accepted ───────────│