WebSocket Protocol
The Server module is a Javalin-based backend that bridges frontends (CLI, IDE, web) to the TnsAI agent framework via WebSocket. It provides multi-agent sessions, real-time streaming, risk-based tool approval, hybrid RAG search, and an audit trail.
Quick Start
Start a TnsAI server with just a few lines of code. The server opens a WebSocket endpoint that clients can connect to for real-time agent interaction.
TnsServer server = TnsServer.builder()
.port(7777)
.llmClientSupplier(() -> LLMClientFactory.create("openai", "gpt-4o", 0.7f))
.idleTimeout(Duration.ofMinutes(30))
.build();
server.start();
// WebSocket endpoint: ws://localhost:7777/chatThe builder requires a Supplier<LLMClient> -- the factory is called once per agent creation. Default port is 7777. An idle shutdown manager auto-terminates the server after 30 minutes of inactivity.
Connection Lifecycle
Understanding the connection lifecycle helps you build reliable clients. Here is what happens from the moment a client connects to when it disconnects.
- Client opens a WebSocket connection to
/chat WsHandler.onConnectfires, registers the connection withIdleShutdownManager- Client sends a
chatevent with asessionId-- the server creates aSessionInfo(with a default assistant agent) on first access - The connection is registered to that session in
WsConnectionManager-- multiple connections can share one session - On disconnect,
onCloseremoves the connection and notifies the idle manager
Every message must carry the protocol version (v: 1) and a sessionId. The server rejects messages with mismatched versions (PROTOCOL_MISMATCH error).
Protocol v1 Events
Client to Server
These are the events that a client can send to the server. Every event must include v: 1 (protocol version) and a sessionId.
| Type | Fields | Description |
|---|---|---|
chat | message | Send a user message to the session's active agent |
agent_add | role, provider?, model?, goal?, domain?, name?, roles? | Add a new agent to the session |
agent_remove | agentId | Remove an agent from the session |
tool_approve | toolCallId, decision | Respond to a tool approval request (approve, reject, always) |
cancel | -- | Cancel the running chat task and pending approvals |
mcp_tools | tools: McpToolDef[] | Register MCP server tools for the session |
mcp_result | toolCallId, result?, error? | Return the result of an MCP proxy tool call |
set_autonomy | level | Change the autonomy level (FULL_AUTO, SUPERVISED, CAUTIOUS, MANUAL) |
kb_add | content, metadata? | Add a document to the session knowledge base |
kb_search | query, limit?, threshold? | Search the knowledge base |
kb_list | -- | List all documents in the knowledge base |
kb_remove | documentId | Remove a document from the knowledge base |
Server to Client
These are the events that the server sends to connected clients. Your client should handle at least token, done, and error for basic functionality.
| Type | Fields | Description |
|---|---|---|
token | agentId, content | A streamed content token from the LLM |
tool_start | agentId, toolCallId, tool, args, risk | Tool execution has begun |
tool_approve_request | agentId, toolCallId, tool, risk, summary | User approval required for a tool call |
tool_result | agentId, toolCallId, tool, duration, result | Tool execution completed |
agent_state | agentId, status, role, provider?, model? | Agent status change (thinking, idle) |
team_update | agents: AgentInfo[], strategy | Full roster update after agent add/remove |
done | usage: {tokens, cost} | LLM response stream completed |
error | message, code? | Error event with optional error code |
mcp_call | toolCallId, server, tool, args | Request the client to execute an MCP tool |
index_start | path, totalFiles | Directory indexing started |
index_progress | indexedFiles, totalFiles, currentFile | Indexing progress update |
index_done | fileCount, chunkCount | Indexing completed |
autonomy_changed | level | Autonomy level was updated |
audit_entry | toolCallId, agentId, tool, risk, decision, timestamp, summary | Audit trail entry |
kb_added | documentId | Document added to knowledge base |
kb_search_result | results: KbSearchHit[] | Knowledge base search results |
kb_list_result | documents: KbDocInfo[], totalCount | Knowledge base document listing |
kb_removed | documentId, found | Document removal result |
Session Management
Sessions are the server's way of keeping track of ongoing conversations. Each session has its own set of agents, an audit trail, and an autonomy level. The SessionManager creates a session automatically the first time a client sends a message with a given sessionId.
// SessionInfo holds:
// - AgentGroup (one or more agents)
// - AuditService (per-session audit trail)
// - AutonomyLevel (default: SUPERVISED)
// - createdAt / lastActivity timestamps
SessionInfo session = sessionManager.getOrCreate("my-session-id");
session.getGroup().getMembers(); // List<Agent>
session.getAutonomyLevel(); // SUPERVISED
session.getAuditService(); // Per-session audit trailEach session has its own RagService, lazily created on first access via sessionManager.getRag(sessionId).
Multi-Agent Support
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 with specific LLM configuration
{
"v": 1,
"type": "agent_add",
"sessionId": "sess-1",
"role": "reviewer",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"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.
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 Tool Injection
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 Events
Each session has an optional knowledge base for Retrieval-Augmented Generation (RAG). The kb_* events let clients add, search, list, and remove documents in real time, which the agent can then use as context when answering questions.
// 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.
WsHandler Internals
This section covers the internal workings of the WebSocket handler for contributors and advanced users. The WsHandler processes all WebSocket events on the Javalin thread, then dispatches chat work to a virtual-thread executor for non-blocking concurrency.
- Chat routing: Messages are routed to the first agent in the group. The agent runs
streamChatWithTools, which handles the full LLM-tool-LLM loop. - RAG injection: Before sending a message to the LLM, the handler checks if the session has indexed content. If so, it calls
rag.buildContextPrompt(message, 5)to prepend relevant code context. - Cancellation:
cancelevents interrupt the running virtual thread and cancel all pending approval futures. - Keepalive: Plain
"ping"text frames are silently ignored.
REST Endpoints
In addition to the WebSocket endpoint, the server exposes REST APIs for health checking, code search indexing, and audit trail retrieval. These are useful for monitoring, CI/CD integration, and administrative tasks.
| Method | Path | Description |
|---|---|---|
| GET | /health | Aggregated health status (200/503) |
| GET | /health/live | Liveness probe (always 200) |
| GET | /health/ready | Readiness probe (server + components) |
| GET | /api/info | Server info (version, protocol, active sessions) |
| POST | /api/index | Trigger directory indexing |
| GET | /api/index/status | Current index status |
| POST | /api/search | Search the index |
| DELETE | /api/index | Clear the index |
| GET | /api/audit | Retrieve audit trail for a session |
Connection Example
Here is a complete JavaScript example showing how to connect to the server, send a chat message, handle streamed tokens, and respond to tool approval requests.
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;
}
};Tool Approval
The server implements a risk-based tool approval system that classifies tool calls by danger level, checks them against the session's autonomy setting, and either auto-approves or blocks the agent's virtual thread until the user responds via WebSocket.
SCOP Bridge
The Integration module connects TnsAI with the SCOP (Self-Constructing Object Program) framework using reflection-based discovery. No compile-time dependency on SCOP is required -- detection happens at runtime. It also provides an HTTP fallback transport for 11 LLM providers when Core's native SPI implementations are unavailable.