Running the server
TnsServer serves agents over a WebSocket at /chat, plus a few HTTP endpoints for health, indexing and audit. Each new session starts with an assistant agent, and a client can add more. AgentFactory builds every one of them with shell, file and git tools. This page covers starting the server from code, the tools those agents get, and when the server shuts itself down. To run the server from the command line instead, see Server.
Example
package com.example.tnsai.docs;
import com.tnsai.agents.Agent;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.OllamaClient;
import com.tnsai.server.TnsServer;
import com.tnsai.server.agent.AgentFactory;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
/** Compilation contract for server/run.md. */
public final class ServerRunExample {
private ServerRunExample() {
}
static TnsServer run() {
TnsServer server = TnsServer.builder()
.port(7777)
.llmClientSupplier(() -> new OllamaClient("qwen3:8b")) // called once per agent
.idleTimeout(Duration.ofMinutes(30))
.build();
server.start(); // returns once the listener is up
return server;
}
static void runUntilIdle(Supplier<LLMClient> llm) throws InterruptedException {
CountDownLatch idle = new CountDownLatch(1);
AtomicReference<TnsServer> ref = new AtomicReference<>();
TnsServer server = TnsServer.builder()
.llmClientSupplier(llm)
.idleTimeout(Duration.ofMinutes(5))
.onIdleShutdown(() -> { // replaces the built-in stop
ref.get().stop();
idle.countDown();
})
.build();
ref.set(server);
server.start();
idle.await();
}
static Agent developer(Supplier<LLMClient> llm) {
AgentFactory factory = new AgentFactory(llm, Path.of("/srv/project")); // the tools' sandbox
return factory.createAgent("dev-1", "developer");
}
}build() throws NullPointerException when no llmClientSupplier is set. Every other option has a default.
Builder options
| Option | Default | Notes |
|---|---|---|
port | 7777 | |
llmClientSupplier | required | Called once for each agent the server creates. |
idleTimeout | 30 minutes | See Idle shutdown. |
onIdleShutdown | stop the server | Replaces the built-in stop. |
bindPolicy | loopback, 127.0.0.1 | See Server hardening. |
authConfig | disabled | Bearer token. Required for any non-loopback bind. |
originPolicy | loopback origins | Checked on the WebSocket upgrade. |
workspaceConfig | process working directory | Limits the paths /api/index may index. |
healthRegistry | the shared HealthRegistry.getInstance() | Checks behind /health and /health/ready. |
liabilitySink, principalProvider | none, local identity | Accountability records for each agent. |
idempotencyStore | in memory |
start() returns once the listener is up. It also registers a JVM shutdown hook that stops the server. stop() stops the idle timer, cancels pending approvals and running chat work, and then stops the HTTP listener.
Agents
The server builds every session agent through AgentFactory. You can also use the factory on its own, as developer() does in the example.
The role name picks a built-in goal: assistant, developer, reviewer, architect or tester. Any other name gets a generic goal for that role. The domain defaults to tnsai-server. The longer createAgent overloads take an explicit goal, domain, provider, model and extra tools.
Each agent also gets:
- a principal from the factory's
AgentPrincipalProvider; - an unrestricted authority scope that lasts 24 hours;
- a search knowledge entry over the session's index (see Session index).
Provider resolution:
- With no provider, the factory calls the supplier.
- With
ollama, it creates anOllamaClientfor the given model, or forglm-5:cloudwhen no model is given. - Any other provider name throws
IllegalArgumentException.
Tools
The tools are sandboxed to the factory's working directory. That is the directory you pass to AgentFactory. The server always uses the process working directory (user.dir), not the workspace allowlist.
| Tool | What it does |
|---|---|
shell | Runs the command with sh -c in the working directory. It times out after 30 seconds. Commands containing ../ are refused, and so are sudo, su, doas, pkexec and runas. |
file_read | Reads a file, up to 512,000 bytes. A larger file throws IOException. |
file_write | Writes a file and creates missing parent directories. |
git_read | Runs status, log, diff, blame, branch, show or stash list. Flags that delete or copy branches are refused. |
git_write | Runs add, commit, push, pull, checkout, branch, merge, rebase, stash, tag or reset. A force push to main or master is refused. |
file_read and file_write refuse a path that leaves the working directory after normalization. They do not resolve symbolic links. The git tools time out after 60 seconds for push and pull and after 30 seconds for everything else. shell and the git tools cut stdout at 10,000 characters and stderr at 5,000.
Over the WebSocket, every tool call first goes through the approval filter, which classifies its risk; see Tool approval. An agent you build with AgentFactory directly has no approval filter. Its tools run as soon as the model calls them.
Session index
Each session has its own index. It combines BM25 keyword scores (weight 0.6) with vector similarity (weight 0.4). You fill it with POST /api/index for a directory inside the workspace allowlist, or with documents sent over the WebSocket. The session's agent searches it on every chat turn and reads the top 5 results. The index is discarded with the session. For the message formats, see WebSocket protocol.
Idle shutdown
The server counts open WebSocket connections:
start()arms the timer, so a server that nobody connects to stops afteridleTimeout.- Each new connection cancels the timer.
- When the last connection closes, the timer starts again.
- When the timer fires, the server checks the connection count again before acting.
The timeout is counted in whole seconds, so a timeout under one second fires at once.
The default action stops the server. An onIdleShutdown action replaces that stop, so if the action does not call stop() itself, the server keeps running.
Related
- Server: run the server from the command line.
- Tool call hooks: log or run commands around each tool call.
- Tool approval: risk levels and approval requests.
- WebSocket protocol: the messages a client sends and receives.
- Server hardening: binding, tokens, origins and the workspace allowlist.