Module 37: Streamable HTTP and WebSocket
Serve an agent over the network instead of stdio, with one agent runtime per connection, and connect to it with both Streamable HTTP and WebSocket at once. See Transports for the identity and routing tables this module builds on, and the RFD Streamable HTTP and WebSocket Transport for the wire rules.What Youâll Learn
- Why a network listener takes an
AcpAgentFactory, not an agent, and what that means for state AcpAgentSupport.Builder#buildFactory()for an annotated agent behind a listener, and why the bean must be thread-safe- Starting
StreamableHttpAcpAgentTransporton port0and reading the bound port withgetPort() - Connecting with
StreamableHttpAcpClientTransportandWebSocketAcpClientTransportto the same listener - Plain
http://negotiating HTTP/2 (h2c) automatically - The built-in listenerâs limits in 0.80.0: no TLS of its own
The Code
One factory, many agents
Over stdio, one process is one connection is one agent. A network listener accepts many connections, so it takes a factory that builds a fresh agent for every one, bound to that connectionâs own transport. The recommended way to get one is an annotated agentâsbuildFactory():
AtomicInteger counting prompts across every connection, in this moduleâs AnnotatedHttpAgent) must be thread-safe; per-session state belongs in a concurrent map keyed by session id instead, as in Module 33.
The lower-level builder API builds a factory from a lambda instead, for cases that need a fresh agent instance (not just a shared bean) per connection:
connection number, so every agent instance remembers which connection it belongs to: this is what lets the rest of this moduleâs demo show that two concurrent clients get two different agents. The walkthrough below uses this builder factory, since its per-connection closure state makes the âdifferent agent per connectionâ behavior easy to see; either factory style works identically with the listener.
Starting the listener
StreamableHttpAcpAgentTransport (module acp-streamable-http-jetty) runs an embedded Jetty listener on /acp that speaks Streamable HTTP (POST requests, SSE responses: one stream for the connection, one per ACP session, routed by the Acp-Connection-Id and Acp-Session-Id headers), HTTP/2 over plain http:// (h2c), and a WebSocket upgrade on the same path. The listener itself is not an AcpAgentTransport; each connection gets its own, created inside the SDK.
Connecting with both transports at once
AcpClient.sync(transport), initialize(), newSession(...), prompt(...) all work exactly the same regardless of transport. Each connection gets its own agent from the factory, so the HTTP client and the WebSocket client see different connection numbers in their answers, but opening a second ACP session on the same HTTP connection is still served by the same agent instance.
Checking the HTTP/2 upgrade
http://. A server that doesnât upgrade just makes the client pin HTTP/1.1; both work.
Closing
Limits of the built-in listener in 0.80.0: a plain connector only; terminate TLS in front of it, or mount
StreamableHttpAcpServlet (from acp-http-servlet) in your own servlet container instead, which serves Streamable HTTP, SSE and the WebSocket upgrade together on that containerâs own port. Module 38 shows the Spring Boot setup for both the standalone listener and the servlet case.Source Code
View on GitHubRunning the Example
buildFactory(). No API key required.