Skip to content

Commit 53c4ec8

Browse files
committed
docs: add HTTP/WebSocket transport documentation and tests
- Update README.md to mention HTTP/WebSocket transport and examples - Add HTTP/WebSocket section to docs/quickstart.md with usage examples - Add dedicated docs/http-transport.md with architecture, API reference, and server/client examples - Add http-transport.md to mkdocs.yml navigation - Add tests/test_http_transport.py with 13 tests covering: - WebSocketStreamAdapter lifecycle (start, close, double-start) - Bidirectional message flow (receive → reader, writer → send) - EOF handling on WebSocket disconnect - StarletteWebSocketWrapper delegation - End-to-end agent ↔ client communication over WebSocket adapters
1 parent 6614d69 commit 53c4ec8

File tree

5 files changed

+514
-2
lines changed

5 files changed

+514
-2
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ uv add agent-client-protocol
2020

2121
- **Spec parity:** Generated Pydantic models in `acp.schema` track every ACP release so payloads stay valid.
2222
- **Runtime ergonomics:** Async base classes, stdio JSON-RPC plumbing, and lifecycle helpers keep custom agents tiny.
23-
- **Examples ready:** Streaming, permissions, Gemini bridge, and duet demos live under `examples/`.
23+
- **Multiple transports:** Stdio for local agents, HTTP/WebSocket for remote deployments — same `Connection` API for both.
24+
- **Examples ready:** Streaming, permissions, Gemini bridge, HTTP/WebSocket, and duet demos live under `examples/`.
2425
- **Helper builders:** `acp.helpers` mirrors the Go/TS SDK APIs for content blocks, tool calls, and session updates.
2526
- **Contrib utilities:** Session accumulators, tool call trackers, and permission brokers share patterns from real deployments.
2627

@@ -53,9 +54,10 @@ See real adopters like kimi-cli in the [Use Cases list](https://agentclientproto
5354
## Project layout
5455

5556
- `src/acp/`: runtime package (agents, clients, transports, helpers, schema bindings, contrib utilities)
57+
- `src/acp/http/`: HTTP/WebSocket transport — `WebSocketStreamAdapter`, `connect_http_agent`, and Starlette wrapper
5658
- `schema/`: upstream JSON schema sources (regenerate via `make gen-all`)
5759
- `docs/`: MkDocs content backing the published documentation
58-
- `examples/`: runnable scripts covering stdio orchestration patterns
60+
- `examples/`: runnable scripts covering stdio and HTTP/WebSocket orchestration patterns
5961
- `tests/`: pytest suite with golden fixtures and optional Gemini coverage
6062

6163
## Developer commands

docs/http-transport.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# HTTP/WebSocket Transport
2+
3+
The `acp.http` module provides a WebSocket-based transport layer for ACP, enabling agents and clients to communicate over HTTP instead of stdio. This is useful for deploying agents as remote web services.
4+
5+
## Overview
6+
7+
While stdio transport works well for local agents spawned as child processes, many deployment scenarios require network-based communication:
8+
9+
- Agents running on cloud infrastructure (e.g., AWS Bedrock AgentCore)
10+
- Multiple clients connecting to a shared agent service
11+
- Agents behind load balancers or API gateways
12+
13+
The HTTP/WebSocket transport uses the same `Connection` API as stdio, so existing agent and client code works without changes.
14+
15+
## Architecture
16+
17+
The adapter uses a 4-queue architecture to bridge WebSocket messages with asyncio streams:
18+
19+
```
20+
INCOMING: WebSocket.recv() → StreamReader.feed_data() → ACP Connection
21+
OUTGOING: ACP Connection → StreamWriter.write() → deque buffer → WebSocket.send()
22+
```
23+
24+
Two background tasks pump messages through the bridge:
25+
26+
- **Receive loop**: Reads from WebSocket, feeds bytes to `asyncio.StreamReader`
27+
- **Send loop**: Drains the outgoing `deque` buffer and sends via WebSocket
28+
29+
This design allows `AgentSideConnection` and `ClientSideConnection` to work unchanged — they read/write asyncio streams as usual.
30+
31+
## Components
32+
33+
### `WebSocketStreamAdapter`
34+
35+
Bridges any WebSocket connection with asyncio `StreamReader`/`StreamWriter`:
36+
37+
```python
38+
from acp.http import WebSocketStreamAdapter
39+
40+
adapter = WebSocketStreamAdapter(websocket)
41+
await adapter.start()
42+
43+
# Use adapter.reader and adapter.writer with ACP Connection
44+
conn = AgentSideConnection(agent, adapter.writer, adapter.reader)
45+
46+
# Clean up
47+
await adapter.close()
48+
```
49+
50+
### `connect_http_agent`
51+
52+
Async context manager for client-side WebSocket connections:
53+
54+
```python
55+
from acp.http import connect_http_agent
56+
57+
async with connect_http_agent(client, "ws://localhost:8080/ws") as conn:
58+
await conn.initialize(protocol_version=PROTOCOL_VERSION)
59+
session = await conn.new_session(mcp_servers=[], cwd=".")
60+
await conn.prompt(session_id=session.session_id, prompt=[text_block("Hello")])
61+
```
62+
63+
Accepts optional `**ws_kwargs` passed through to `websockets.connect()` (e.g., `max_size`, `extra_headers`, `compression`).
64+
65+
### `WebSocketLike` Protocol
66+
67+
Any object implementing `recv()`, `send()`, and `close()` works with the adapter:
68+
69+
```python
70+
class WebSocketLike(Protocol):
71+
async def recv(self) -> str | bytes: ...
72+
async def send(self, data: str | bytes) -> None: ...
73+
async def close(self) -> None: ...
74+
```
75+
76+
### `StarletteWebSocketWrapper`
77+
78+
Pre-built wrapper for Starlette's WebSocket (which uses `receive_text()`/`send_text()` instead of `recv()`/`send()`):
79+
80+
```python
81+
from acp.http import StarletteWebSocketWrapper
82+
83+
wrapped = StarletteWebSocketWrapper(starlette_websocket)
84+
adapter = WebSocketStreamAdapter(wrapped)
85+
```
86+
87+
## Server-Side Example
88+
89+
Serve an ACP agent over WebSocket using Starlette:
90+
91+
```python
92+
from starlette.applications import Starlette
93+
from starlette.routing import WebSocketRoute
94+
from starlette.websockets import WebSocket
95+
96+
from acp.agent.connection import AgentSideConnection
97+
from acp.http import StarletteWebSocketWrapper, WebSocketStreamAdapter
98+
99+
async def websocket_endpoint(websocket: WebSocket):
100+
await websocket.accept()
101+
wrapped = StarletteWebSocketWrapper(websocket)
102+
adapter = WebSocketStreamAdapter(wrapped)
103+
104+
await adapter.start()
105+
106+
agent = MyAgent()
107+
conn = AgentSideConnection(
108+
to_agent=agent,
109+
input_stream=adapter.writer,
110+
output_stream=adapter.reader,
111+
listening=False,
112+
)
113+
114+
await conn.listen()
115+
await adapter.close()
116+
117+
app = Starlette(routes=[WebSocketRoute("/ws", websocket_endpoint)])
118+
```
119+
120+
See `examples/http_echo_agent.py` for a complete runnable server.
121+
122+
## Client-Side Example
123+
124+
Connect to a remote agent:
125+
126+
```python
127+
from acp.http import connect_http_agent
128+
from acp import PROTOCOL_VERSION, text_block
129+
from acp.schema import ClientCapabilities, Implementation
130+
131+
async with connect_http_agent(MyClient(), "ws://agent.example.com/ws") as conn:
132+
await conn.initialize(
133+
protocol_version=PROTOCOL_VERSION,
134+
client_capabilities=ClientCapabilities(),
135+
client_info=Implementation(name="my-client", title="My Client", version="0.1.0"),
136+
)
137+
session = await conn.new_session(mcp_servers=[], cwd="/workspace")
138+
await conn.prompt(session_id=session.session_id, prompt=[text_block("Hello!")])
139+
```
140+
141+
See `examples/http_client.py` for a complete runnable client with interactive prompt.
142+
143+
## Dependencies
144+
145+
- **`websockets>=12.0`** — Required (core dependency). Used by `connect_http_agent` for client-side connections.
146+
- **`starlette`** — Optional, for server-side `StarletteWebSocketWrapper`. Install separately: `pip install starlette uvicorn`.
147+
148+
## Limitations
149+
150+
- Messages use newline-delimited JSON format (one JSON-RPC message per line), consistent with stdio transport.
151+
- Default maximum WebSocket message size is 50MB (matching stdio buffer limit). Override via `max_size` kwarg.
152+
- The `StarletteWebSocketWrapper` currently handles text frames only. Binary WebSocket frames are converted to UTF-8.

docs/quickstart.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,48 @@ Open the Agents panel and start the session. Each message you send should be ech
8181

8282
Any ACP client that communicates over stdio can spawn the same script; no additional transport configuration is required.
8383

84+
### HTTP/WebSocket (remote agents)
85+
86+
For agents deployed as web services (e.g., on AWS Bedrock AgentCore), you can connect via WebSocket instead of stdio. Start the HTTP echo agent server:
87+
88+
```bash
89+
# Terminal 1: Start the server
90+
pip install starlette uvicorn
91+
python examples/http_echo_agent.py
92+
```
93+
94+
Then connect a client from another terminal:
95+
96+
```bash
97+
# Terminal 2: Connect via WebSocket
98+
python examples/http_client.py ws://localhost:8080/ws
99+
```
100+
101+
Or connect programmatically:
102+
103+
```python
104+
import asyncio
105+
from acp import PROTOCOL_VERSION, text_block
106+
from acp.http import connect_http_agent
107+
from acp.schema import ClientCapabilities, Implementation
108+
109+
async def main():
110+
async with connect_http_agent(MyClient(), "ws://localhost:8080/ws") as conn:
111+
await conn.initialize(
112+
protocol_version=PROTOCOL_VERSION,
113+
client_capabilities=ClientCapabilities(),
114+
client_info=Implementation(name="my-client", title="My Client", version="0.1.0"),
115+
)
116+
session = await conn.new_session(mcp_servers=[], cwd=".")
117+
await conn.prompt(session_id=session.session_id, prompt=[text_block("Hello!")])
118+
119+
asyncio.run(main())
120+
```
121+
122+
The `connect_http_agent` context manager handles WebSocket connection lifecycle and uses the same `ClientSideConnection` API as stdio, so all existing client code works unchanged.
123+
124+
> **Note:** The server-side adapter requires `starlette` and `uvicorn` (listed as dev dependencies). For custom server frameworks, implement the `WebSocketLike` protocol (`recv`, `send`, `close`) and pass to `WebSocketStreamAdapter`.
125+
84126
### Programmatic launch
85127

86128
Prefer to drive agents directly from Python? The `spawn_agent_process` helper wires stdio and lifecycle management for you:

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ copyright: Maintained by <a href="https://github.com/psiace">psiace</a>.
1010
nav:
1111
- Home: index.md
1212
- Quick Start: quickstart.md
13+
- HTTP/WebSocket Transport: http-transport.md
1314
- Use Cases: use-cases.md
1415
- Experimental Contrib: contrib.md
1516
- Releasing: releasing.md

0 commit comments

Comments
 (0)