WebSocket Protocol
1. Introduction to WebSocket
WebSocket is a protocol for bidirectional communication over a single TCP connection. It is well suited for scenarios that require persistent connections, low latency, and continuous server-to-client message delivery. The client first sends a standard HTTP request, and the server upgrades the protocol by using Upgrade: websocket. After the upgrade is successful, the connection no longer follows the HTTP model of one request and one response. Instead, the client and server exchange message frames with each other.
The DolphinX HTTP API and WebSocket API share the same set of business capabilities. The difference is that HTTP expresses a call by using method + path + query + body. After a WebSocket connection is established, business calls are represented as JSON request frames, where action identifies the API to call, and payload carries the request parameters.
Use WebSocket in the following scenarios:
- You need streaming conversations, real-time display of model output, or context processing events.
- The client needs to reuse the same connection to send multiple consecutive requests.
- You need to use requestId / correlationId to match multiple requests and responses on the same connection.
For occasional queries, administrative operations, or use cases that do not require real-time streaming responses, HTTP calls are usually simpler.
2. Connection
The WebSocket endpoint uses the same path as HTTP and is connected through a protocol upgrade:
ws://<host>:<port>/agent-bus/v1
wss://<host>:<port>/agent-bus/v1
During the handshake, the client should declare the agent-bus subprotocol and include the token obtained after login in the Authorization header:
Sec-WebSocket-Protocol: agent-bus
Authorization: Bearer <token>
Example of sending a WebSocket request in Python:
import asyncio
import json
import websockets
async def main():
token = "<token>"
url = "ws://localhost:8848/agent-bus/v1"
async with websockets.connect(
url,
additional_headers={"Authorization": f"Bearer {token}"},
subprotocols=["agent-bus"],
) as ws:
request = {
"type": "REQUEST",
"requestId": "req_001",
"action": "agent.accessible",
}
await ws.send(json.dumps(request, ensure_ascii=False))
response = await ws.recv()
print(response)
asyncio.run(main())
3. Message Format
All WebSocket messages are in JSON format.
Request Frame
The client sends a REQUEST frame to call an API. A request frame recognizes only the following top-level parameters. All other parameters are ignored:
| Parameters | Required | Description |
|---|---|---|
| type | Yes | Must be "REQUEST". |
| action | Yes | The WebSocket action name. See the Action Mapping Table below. |
| requestId | No, but recommended | The request ID generated by the client. The server returns the same value in the correlationId field of the response frame. |
| agentId | Depends on the action | The agent ID. Required when the corresponding HTTP API contains
{agentId} or the business logic requires an
agent to be specified. |
| sessionId | Depends on the action | The session ID. Required when the corresponding HTTP API contains
{sessionId} or the business logic requires a
session to be specified. |
| payload | Depends on the action | JSON object that carries the HTTP request body, query string, and path parameters other than agentId / sessionId. |
{
"type": "REQUEST",
"requestId": "req_001",
"action": "chat.completions",
"agentId": "agent_xxx",
"sessionId": "sess_001",
"payload": {
"message": "hello",
"stream": true,
"contextEvent": true
}
}
Response Frame
A standard non-streaming response returns a single RESPONSE frame. The client can use correlationId to match the requestId in the request.
{
"type": "RESPONSE",
"msgId": "...",
"correlationId": "req_001",
"action": "chat.completions",
"agentId": "agent_xxx",
"sessionId": "sess_001",
"data": {}
}
Streaming Frame
A streaming request returns multiple messages. Each message is a standalone JSON object and is not wrapped in the HTTP SSE text format event: / data:. The client should continue reading until it receives STREAM_END or STREAM_ERROR.
{"type":"STREAM_START","correlationId":"req_001","action":"chat.completions","data":{"requestId":"req_001","model":"<resolved-model>"}}
{"type":"STREAM_REASONING","correlationId":"req_001","action":"chat.completions","data":{"reasoning":"..."}}
{"type":"STREAM_CHUNK","correlationId":"req_001","action":"chat.completions","data":{"content":"..."}}
{"type":"STREAM_END","correlationId":"req_001","action":"chat.completions","data":{"metadata":{}}}
WebSocket chat completion supports contextEvent=true. When enabled, processes such as context compression are returned in EVENT messages and use the same correlationId.
Error Frame
{
"type": "ERROR",
"msgId": "...",
"correlationId": "req_001",
"action": "chat.completions",
"agentId": "agent_xxx",
"sessionId": "sess_001",
"error": {
"code": "BUS_SESSION_NOT_FOUND",
"message": "session not found"
},
"headers": {
"busErrorCode": "BUS_SESSION_NOT_FOUND",
"httpStatus": "404"
}
}
4. Convert HTTP Requests to WebSocket Requests
To convert an HTTP request to a WebSocket request, follow these steps:
- Find the action that corresponds to the HTTP API in the Action Mapping Table.
- Put
{agentId}/{sessionId}from the HTTP API into the top-level agentId / sessionId fields of the request frame. - Put other path parameters, query parameters, and JSON request body fields into payload.
- Generate a requestId and send the complete JSON request frame.
- Read the messages returned by the server, and use correlationId in the response to match the request's requestId.
Do not include the same field at the top level and in payload with different values; otherwise, the server rejects the request. For example, if agentId is already provided at the top level, do not pass a different value in payload.agentId.
Example 1: GET Request with Path Parameters and Query Parameters
HTTP Request:
GET /agent-bus/v1/session/sess_001/messages?startSeq=1&endSeq=20
Authorization: Bearer <token>
Conversion Rules:
| Location in HTTP | HTTP Value | Location in WebSocket |
|---|---|---|
| Path | sess_001 | Top-level sessionId |
| Query Parameters | startSeq=1 | payload.startSeq |
| Query Parameters | endSeq=20 | payload.endSeq |
| URL | GET/session/{id}/messages | action: "session.getMessages" |
WebSocket request frame sent:
{
"type": "REQUEST",
"requestId": "req_42",
"action": "session.getMessages",
"sessionId": "sess_001",
"payload": {
"startSeq": 1,
"endSeq": 20
}
}}
Example 2: GET Request with agentId Moved from the Query Parameters to the Top Level
HTTP Request:
GET /agent-bus/v1/session/list?agentId=agent_xxx&limit=50&offset=0
Authorization: Bearer <token>
Conversion Rules:
| Location in HTTP | HTTP Value | Location in WebSocket |
|---|---|---|
| Query Parameters | agentId=agent_xxx | Top-Level agentId |
| Query Parameters | limit=50 | payload.limit |
| Query Parameters | offset=0 | payload.offset |
| URL | GET/session/list | action: "session.list" |
WebSocket request frame sent:
{
"type": "REQUEST",
"requestId": "req_43",
"action": "session.list",
"agentId": "agent_xxx",
"payload": {
"limit": 50,
"offset": 0
}
}
Example 3: POST Request with Path Parameters and JSON Request Body
HTTP Request:
POST /agent-bus/v1/workspace/sess_001/replace
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "main.dos",
"hash": "xxh64:...",
"replacements": [
{
"old": "return 1",
"new": "return 2"
}
]
}
Conversion Rules:
| Location in HTTP | HTTP Value | Location in WebSocket |
|---|---|---|
| Path | sess_001 | Top-level sessionId |
| Request Body | name | payload.name |
| Request Body | hash | payload.hash |
| Request Body | replacements | payload.replacements |
| URL | POST/workspace/{sessionId}/replace | action: "workspace.replace" |
WebSocket request frame sent:
{
"type": "REQUEST",
"requestId": "req_44",
"action": "workspace.replace",
"sessionId": "sess_001",
"payload": {
"name": "main.dos",
"hash": "xxh64:...",
"replacements": [
{
"old": "return 1",
"new": "return 2"
}
]
}
}
5. Supported Actions
WebSocket supports the actions listed in the following table.
| Action | Corresponding HTTP API |
|---|---|
| agent.accessible | GET/agent/accessible |
| agent.get | GET/agent/{id} |
| agent.llm.selectable | GET/agent/{id}/llm/selectable |
| session.create | POST/session/create |
| session.get | GET/session/{id} |
| session.update | PUT/session/{id} |
| session.delete | DELETE/session/{id} |
| session.list | GET/session/list |
| session.getMessages | GET/session/{id}/messages |
| session.generateSummary | POST/session/{id}/summary |
| chat.completions | POST/chat/completions |
| skill.catalog | GET/skill/{agentId}/catalog |
| skill.getInstruction | GET/skill/{id}/instruction |
| skill.files.list | GET/skill/{id}/files/list |
| skill.files.get | GET/skill/{id}/files/get |
| skill.files.search | POST/skill/{id}/files/search |
| skill.files.batchGet | POST/skill/{id}/files/batchGet |
| workspace.list | GET/workspace/{sessionId}/files |
| workspace.read | GET/workspace/{sessionId}/file |
| workspace.write | POST/workspace/{sessionId}/file |
| workspace.delete | DELETE/workspace/{sessionId}/file |
| workspace.search | POST/workspace/{sessionId}/search |
| workspace.replace | POST/workspace/{sessionId}/replace |
| network.fetch | POST/network/fetch |
| mcp.tools.call | POST/mcp/tools/call |
| context.preview | POST/context/preview |
| llm.complete | POST/llm/complete |
