# Agent Client Protocol (ACP) Internal Architecture in Goose

> Explore the internal architecture of the Agent Client Protocol ACP in block/goose. Discover its multi-layered async server implementation for the Goose agent API.

- Repository: [Block Open Source/goose](https://github.com/block/goose)
- Tags: internals
- Published: 2026-04-05

---

**The Agent Client Protocol (ACP) in block/goose is implemented as a multi-layered async server that wraps the core Goose agent API, exposing it via JSON-RPC over HTTP, WebSocket, or stdio through per-session agents and bidirectional channel adapters.**

The ACP implementation resides in the `crates/goose-acp` directory of the `block/goose` repository. It acts as a translation layer that accepts **Agent Client Protocol** messages from external clients and maps them to the native Goose agent interface, maintaining full compatibility with the existing permission system, extension registry, and tool execution framework.

## Core Architecture Layers

The ACP stack is organized into tightly-coupled layers, each handling a specific responsibility in the communication pipeline.

### Server Factory and Configuration

The entry point for ACP sessions is the **server factory**, defined in [`crates/goose-acp/src/server_factory.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/server_factory.rs). This module reads the user's [`goose.yaml`](https://github.com/block/goose/blob/main/goose.yaml) configuration, initializes the selected **Provider** (model backend), and constructs an `AcpServer` instance capable of spawning new agents.

When the binary runs `goose acp`, it creates an `AcpServerFactoryConfig` containing builtins, data directories, and configuration paths. The factory then instantiates the server:

```rust
let factory = AcpServerFactoryConfig { … };
let server = AcpServer::new(factory);

```

### GooseAcpAgent and Session State

Each incoming connection receives a dedicated `GooseAcpAgent` instance managed in [`crates/goose-acp/src/server.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/server.rs). This struct holds per-session state including the conversation history, pending tool requests, and cancellation tokens.

The agent implements the full Goose interface, handling initialization, extension loading, permission flows, and tool execution. Unlike the standard Goose server which may share state, the ACP version creates isolated sessions via `AcpServer::create_agent` to ensure client separation.

### Transport Implementations

The transport layer exposes two concrete implementations in [`crates/goose-acp/src/transport/http.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/transport/http.rs) and [`crates/goose-acp/src/transport/websocket.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/transport/websocket.rs). Both use Axum routers to handle incoming connections.

Each transport manages a `TransportSession` stored in a `RwLock<HashMap<String, TransportSession>>` within `HttpState` or `WsState`. When a client sends a JSON-RPC request to the `/acp` endpoint, the server routes it through `handle_initialize`, `handle_request`, or `handle_notification_or_response` depending on the message type.

### Async I/O Adapters

To bridge the gap between Axum's channel-based communication and the core agent's expectations, [`crates/goose-acp/src/adapters.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/adapters.rs) provides `ReceiverToAsyncRead` and `SenderToAsyncWrite` wrappers. These adapters convert `mpsc::Receiver<String>` and `mpsc::Sender<String>` into Tokio `AsyncRead` and `AsyncWrite` traits.

This abstraction allows the generic ACP server code to remain transport-agnostic while still supporting HTTP Server-Sent Events (SSE) and WebSocket frames.

### Tool Metadata and Filesystem Extensions

The [`crates/goose-acp/src/tools.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/tools.rs) module adds ACP-specific metadata to tool results through the `with_acp_aware_meta()` method on `CallToolResult`. Clients can query `is_acp_aware()` to identify tool calls that originated from an ACP session, enabling specialized UI rendering.

Additionally, [`crates/goose-acp/src/fs.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/fs.rs) exposes a virtual filesystem extension that allows ACP clients to read and write workspace files directly through the protocol.

## Request Lifecycle

Understanding how these layers interact requires examining the message flow from connection initialization to tool execution.

1. **Transport Selection**: The server starts either HTTP or WebSocket listeners using `transport::http::create_router` or `transport::websocket::create_router`, both sharing the same state management logic.

2. **Session Initialization**: When a client sends the initial JSON-RPC `initialize` request, `HttpState::create_session` triggers:
   - Creation of bidirectional mpsc channels (`to_agent_tx`, `from_agent_rx`)
   - Instantiation of a fresh `GooseAcpAgent` via `AcpServer::create_agent`
   - Wrapping channels with `ReceiverToAsyncRead` and `SenderToAsyncWrite` adapters
   - Launching the core agent loop in a background task via `crate::server::serve`

3. **Session Tracking**: The server generates a UUID for the session, returning it in the `Acp-Session-Id` header and storing the `TransportSession` in the shared state map.

4. **Message Processing**: Subsequent POST requests to `/acp` execute the following:
   - Verify session existence via `has_session`
   - Serialize incoming JSON-RPC messages to `to_agent_tx`
   - Stream agent responses from `from_agent_rx` to the client as **Server-Sent Events** (`text/event-stream`)

5. **Tool Execution**: When the agent invokes tools, results are optionally enriched with `with_acp_aware_meta()` from [`tools.rs`](https://github.com/block/goose/blob/main/tools.rs), marking them for ACP-specific handling.

6. **Permission Handling**: The `GooseAcpAgent` reuses the standard Goose permission manager and extension registry, ensuring ACP clients have access to developer tools, MCP extensions, and custom providers without code duplication.

## Client Integration Example

The `goose-sdk` crate provides a reference implementation showing how to connect to an ACP server via stdio. This pattern works for HTTP and WebSocket transports with minor modifications.

The following example spawns `goose acp`, connects via `sacp::ByteStreams`, and drives a conversation:

```rust
// crates/goose-sdk/examples/acp_client.rs
let transport = sacp::ByteStreams::new(
    child_stdin.compat_write(),
    child_stdout.compat()
);

Client::builder()
    .name("acp-client-example")
    .on_receive_notification(|notification, _| {
        if let SessionUpdate::AgentMessageChunk(chunk) = &notification.update {
            if let ContentBlock::Text(text) = &chunk.content {
                print!("{}", text.text);
            }
        }
        Ok(())
    }, sacp::on_receive_notification!())
    .on_receive_request(|req, responder, _| {
        let opt = req.options.first().map(|o| o.option_id.clone());
        responder.respond(match opt {
            Some(id) => RequestPermissionResponse::new(
                RequestPermissionOutcome::Selected(
                    SelectedPermissionOutcome::new(id))),
            None => RequestPermissionResponse::new(
                RequestPermissionOutcome::Cancelled),
        })
    }, sacp::on_receive_request!())
    .connect_with(transport, |cx| async move {
        // Initialize the session
        let _ = cx.send_request(
            InitializeRequest::new(ProtocolVersion::LATEST)
        ).await?;
        
        // Optional: fetch available extensions
        let _ = cx.send_request(GetExtensionsRequest {}).await?;
        
        // Execute a prompt
        cx.build_session_cwd()?.run_until(|mut sess| async {
            sess.send_prompt(&prompt)?;
            let _ = sess.read_to_string().await?;
            Ok(())
        }).await
    })
    .await?;

```

This implementation demonstrates auto-approving permissions, handling streaming agent messages, and managing session state through the ACP protocol.

## Summary

- The **Agent Client Protocol** implementation in `block/goose` is a thin async wrapper around the core agent API, located in `crates/goose-acp`.
- **Architecture** consists of a server factory ([`server_factory.rs`](https://github.com/block/goose/blob/main/server_factory.rs)), per-session agents ([`server.rs`](https://github.com/block/goose/blob/main/server.rs)), dual transport layers ([`transport/http.rs`](https://github.com/block/goose/blob/main/transport/http.rs), [`transport/websocket.rs`](https://github.com/block/goose/blob/main/transport/websocket.rs)), and channel adapters ([`adapters.rs`](https://github.com/block/goose/blob/main/adapters.rs)).
- **Session management** uses UUID-tracked `TransportSession` objects stored in Axum state, with bidirectional mpsc channels converted to `AsyncRead/AsyncWrite` for the agent loop.
- **Communication** flows over HTTP (SSE) or WebSocket as JSON-RPC, supporting initialization, tool calls, and permission requests.
- **Tool results** can be flagged as ACP-aware via `with_acp_aware_meta()` to enable client-specific UI behaviors.
- **Clients** can connect via stdio, HTTP, or WebSocket and receive the full Goose feature set including extensions and permission management.

## Frequently Asked Questions

### What transports does the Goose ACP support?

The Goose ACP supports three transport mechanisms: HTTP with Server-Sent Events (SSE) via the `/acp` endpoint, WebSocket through the `/acp` upgrade path, and stdio for local process communication. All transports share the same session management and agent logic, differing only in how they wrap the bidirectional byte streams.

### How does the ACP handle concurrent sessions?

Each incoming connection triggers the creation of a unique `GooseAcpAgent` instance with isolated state. The `HttpState` maintains a `RwLock<HashMap<String, TransportSession>>` that maps UUIDs to active sessions. When a client sends the `initialize` request, the server generates a new UUID, stores the session channels, and returns the ID in the `Acp-Session-Id` header for subsequent request routing.

### Can ACP clients access the same extensions as the standard Goose CLI?

Yes. The `GooseAcpAgent` reuses the identical extension registry, permission manager, and tool execution framework as the standard Goose server. According to the implementation in [`crates/goose-acp/src/server.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/server.rs), the agent delegates to the core Goose APIs, meaning ACP clients can invoke developer tools, MCP extensions, and custom providers without modification to the underlying logic.

### What is the purpose of the ACP-aware metadata flag on tool results?

The ACP-aware metadata flag, added via `with_acp_aware_meta()` in [`crates/goose-acp/src/tools.rs`](https://github.com/block/goose/blob/main/crates/goose-acp/src/tools.rs), allows clients to distinguish between tool calls originating from an ACP session versus standard tool execution. Clients can check this flag using `is_acp_aware()` to determine whether to render results in an ACP-specific UI element or handle them differently from standard output.