Skip to content
tnsaiDocs
GitHub

WebSocket protocol

Clients talk to TnsServer over one WebSocket endpoint, /chat. This page covers protocol version 1: the events in each direction, sessions and their agents, MCP tool proxying and the session knowledge base. To start a server, see Running the server.

Connection lifecycle

  1. The client opens a WebSocket connection to /chat. When the server has a bearer token (TNSAI_TOKEN or AuthConfig.withToken), the upgrade must carry Authorization: Bearer <token>; a browser WebSocket cannot set that header. See Server hardening.
  2. When the upgrade URL carries a sessionId, the server checks that session's capability token on upgrade. See Server hardening.
  3. The first event binds the connection to its sessionId. A later event with another sessionId gets a FORBIDDEN_SESSION error and is dropped.
  4. The first event for a new sessionId creates the session with one assistant agent. Several connections can share one session.
  5. When any connection of a session closes, the server cancels that session's pending approvals and running chat tasks.
  6. When the last connection of a session closes, the server removes the session. Its agents, audit trail and knowledge base are discarded, and a later event with the same sessionId starts a new, empty session.

Every event must carry the protocol version (v: 1) and a sessionId. An event with another version gets a PROTOCOL_MISMATCH error. A plain ping text frame is ignored, so clients can use it as a keepalive.

Protocol v1 events

Client to server

TypeFieldsDescription
chatmessageSend a user message to the session's active agent
agent_addrole, provider?, model?, goal?, domain?, name?, roles?Add a new agent to the session
agent_removeagentIdRemove an agent from the session
tool_approvetoolCallId, decisionRespond to a tool approval request (approve, reject, always)
cancelNoneCancel the running chat task and pending approvals
mcp_toolstools: McpToolDef[]Register MCP server tools for the session
mcp_resulttoolCallId, result?, error?Return the result of an MCP proxy tool call
set_autonomylevelChange the autonomy level (FULL_AUTO, SUPERVISED, CAUTIOUS, MANUAL)
kb_addcontent, metadata?Add a document to the session knowledge base
kb_searchquery, limit?, threshold?Search the knowledge base
kb_listNoneList all documents in the knowledge base
kb_removedocumentIdRemove a document from the knowledge base

Server to client

TypeFieldsDescription
tokenagentId, contentA streamed content token from the LLM
tool_startagentId, toolCallId, tool, args, riskTool execution has begun
tool_approve_requestagentId, toolCallId, tool, risk, summaryUser approval required for a tool call
tool_resultagentId, toolCallId, tool, duration, resultTool execution completed
agent_stateagentId, status, role, provider?, model?Agent status change (thinking, idle)
team_updateagents: AgentInfo[], strategyFull roster update after agent add/remove
doneusage: {tokens, cost}LLM response stream completed. tokens is the number of streamed content chunks, not model tokens; cost is always 0.0
errormessage, code?Error event with optional error code
mcp_calltoolCallId, server, tool, argsRequest the client to execute an MCP tool
index_startpath, totalFilesDirectory indexing started
index_progressindexedFiles, totalFiles, currentFileIndexing progress update
index_donefileCount, chunkCountIndexing completed
autonomy_changedlevelAutonomy level was updated
audit_entrytoolCallId, agentId, tool, risk, decision, timestamp, summaryAudit trail entry
kb_addeddocumentIdDocument added to knowledge base
kb_search_resultresults: KbSearchHit[]Knowledge base search results
kb_list_resultdocuments: KbDocInfo[], totalCountKnowledge base document listing
kb_removeddocumentId, foundDocument removal result

Sessions

Each session has its own agents, audit trail, autonomy level and knowledge base. The autonomy level starts at SUPERVISED; set_autonomy changes it, and Tool approval explains what each level asks for.

Multiple agents

A session can contain multiple agents with different roles and LLM configurations. You can add and remove agents dynamically at runtime using WebSocket events, which is useful for building team-based workflows where different agents handle different parts of a task.

// Add an agent using the server's configured default LLM supplier
{
  "v": 1,
  "type": "agent_add",
  "sessionId": "sess-1",
  "role": "reviewer",
  "name": "Code Reviewer",
  "goal": "Review code for security issues"
}

Agent IDs are generated as {name}-{8hexchars} (e.g., Code Reviewer-abcd1234). After each add/remove, the server broadcasts a team_update event with the full agent roster, including each agent's tools, roles, and LLM configuration.

The example omits provider, so the new agent uses the server's llmClientSupplier. The only provider you can name is ollama, which uses model, or glm-5:cloud when model is missing. Any other provider gets an error event with code AGENT_ADD_FAILED, and no agent is added. To give agents a cloud model, set it as the server's supplier and omit provider.

After a successful add, the agent_state event reports the provider and model of the agent's actual client, not the request fields.

Multi-role agents can be created by providing a roles array instead of a single goal/domain:

{
  "v": 1,
  "type": "agent_add",
  "sessionId": "sess-1",
  "role": "fullstack",
  "roles": [
    {"name": "frontend", "goal": "Build React UIs", "domain": "web"},
    {"name": "backend", "goal": "Build REST APIs", "domain": "server"}
  ]
}

MCP tools

If your frontend application connects to MCP servers (like a filesystem or database server), you can register those tools with the TnsAI server session. The server wraps each tool as a McpProxyTool and injects them into all agents, enabling a proxy pattern where the server orchestrates and the client executes.

// Client sends mcp_tools event
{
  "v": 1,
  "type": "mcp_tools",
  "sessionId": "sess-1",
  "tools": [
    {
      "server": "filesystem",
      "name": "read_file",
      "description": "Read a file",
      "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}}
    }
  ]
}

When the LLM invokes an MCP tool, the server sends an mcp_call event to the client. The client executes the tool via its local MCP connection and returns the result via mcp_result:

Server -> Client:  mcp_call {toolCallId: "abc", server: "filesystem", tool: "read_file", args: {path: "/src/App.tsx"}}
Client -> Server:  mcp_result {toolCallId: "abc", result: "import React from 'react';..."}

The McpProxyTool uses a CompletableFuture to block the agent's virtual thread until the client responds.

Knowledge base

Each session has its own knowledge base. The kb_* events add, search, list and remove documents in it, and POST /api/index indexes a whole directory into it. The session's agent searches it on every chat turn.

// Add a document
{"v": 1, "type": "kb_add", "sessionId": "s1",
 "content": "React hooks must follow the rules of hooks...",
 "metadata": {"source": "docs", "topic": "react"}}

// Search
{"v": 1, "type": "kb_search", "sessionId": "s1",
 "query": "rules of hooks", "limit": 5, "threshold": 0.3}

// List all documents
{"v": 1, "type": "kb_list", "sessionId": "s1"}

// Remove a document
{"v": 1, "type": "kb_remove", "sessionId": "s1", "documentId": "doc-abc123"}

Search results include documentId, content, score, and metadata for each hit. The kb_list_result event returns document previews (first 100 chars) and content lengths.

How chat runs

  • A chat message goes to the first agent in the session. The agent runs streamChatWithTools, which handles the whole model, tool, model loop.
  • Before each turn, the handler binds the session's knowledge base to the agent, so the agent's knowledge search reads the top 5 results.
  • Chat work runs on virtual threads, off the WebSocket thread.
  • cancel interrupts the running chat task and cancels every pending approval.

HTTP endpoints

The server also serves health, index, search and audit endpoints over HTTP; see Endpoints.

Connection example

This JavaScript client sends one chat message, prints the streamed tokens and approves every tool call. It sends no bearer token, so it only works against a server without a bearer token.

const ws = new WebSocket("ws://localhost:7777/chat");

ws.onopen = () => {
  // Send a chat message
  ws.send(JSON.stringify({
    v: 1,
    type: "chat",
    sessionId: "my-session",
    message: "Explain the WebSocket protocol"
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  switch (data.type) {
    case "token":
      process.stdout.write(data.content);
      break;
    case "tool_approve_request":
      // Auto-approve for this example
      ws.send(JSON.stringify({
        v: 1,
        type: "tool_approve",
        sessionId: data.sessionId,
        toolCallId: data.toolCallId,
        decision: "approve"
      }));
      break;
    case "done":
      console.log("\n[Done]", data.usage);
      break;
    case "error":
      console.error("[Error]", data.message);
      break;
  }
};