Skip to main content

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 StreamableHttpAcpAgentTransport on port 0 and reading the bound port with getPort()
  • Connecting with StreamableHttpAcpClientTransport and WebSocketAcpClientTransport to 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’s buildFactory():
Every connection gets its own agent runtime, but they all dispatch to this one bean. State on the bean itself (an 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:
Each connection’s lambda invocation closes over its own 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

Client code is otherwise unchanged from stdio: 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

The Streamable HTTP client sends this same bodiless GET first, so later requests reuse an already-upgraded HTTP/2 connection over plain 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 GitHub

Running the Example

The demo starts a listener on a free port, probes the HTTP/2 upgrade, connects a Streamable HTTP client and a WebSocket client concurrently (two sessions on the HTTP connection, one on the WebSocket connection), confirms they got different agents, then starts a second listener for an annotated agent built with buildFactory(). No API key required.

Next Module

Module 38: Spring Boot over HTTP: the same transport, wired through acp-autoconfig’s properties instead of by hand.