Skip to content
tnsaiDocs
GitHub

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

OptionDefaultNotes
port7777
llmClientSupplierrequiredCalled once for each agent the server creates.
idleTimeout30 minutesSee Idle shutdown.
onIdleShutdownstop the serverReplaces the built-in stop.
bindPolicyloopback, 127.0.0.1See Server hardening.
authConfigdisabledBearer token. Required for any non-loopback bind.
originPolicyloopback originsChecked on the WebSocket upgrade.
workspaceConfigprocess working directoryLimits the paths /api/index may index.
healthRegistrythe shared HealthRegistry.getInstance()Checks behind /health and /health/ready.
liabilitySink, principalProvidernone, local identityAccountability records for each agent.
idempotencyStorein 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 an OllamaClient for the given model, or for glm-5:cloud when 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.

ToolWhat it does
shellRuns 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_readReads a file, up to 512,000 bytes. A larger file throws IOException.
file_writeWrites a file and creates missing parent directories.
git_readRuns status, log, diff, blame, branch, show or stash list. Flags that delete or copy branches are refused.
git_writeRuns 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 after idleTimeout.
  • 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.