Server Advanced Features
Declarative hooks, agent factory, idle shutdown management, and the RAG pipeline (indexing, chunking, retrieval).
Declarative Hook System
The com.tnsai.server.hooks package provides a YAML-driven hook system that attaches lifecycle callbacks to agents without writing Java code.
Architecture Overview
hooks.yaml --> DeclarativeHookRegistry --> ToolCallListener on Agent
| |
| PRE_TOOL_USE / POST_TOOL_USE
|
DeclarativeHookExecutor
|
COMMAND (shell) / LOG (message)HookEvent
Lifecycle events that hooks can listen for:
| Event | Description |
|---|---|
PRE_TOOL_USE | Before a tool executes |
POST_TOOL_USE | After a tool executes |
ON_START | When the agent starts |
ON_STOP | When the agent stops |
ON_ERROR | When an error occurs |
Parsing supports both camelCase (preToolUse) and UPPER_SNAKE (PRE_TOOL_USE) from YAML.
HookActionType
| Type | Description |
|---|---|
COMMAND | Execute a shell command |
LOG | Log a message |
HookAction
Record defining what to execute when a hook fires.
// Factory methods
HookAction cmd = HookAction.command("echo 'tool finished'");
HookAction log = HookAction.log("Tool ${tool} is about to execute");Fields: type (HookActionType), command (for COMMAND), message (for LOG). Supports ${variable} placeholders that are substituted at execution time.
HookMatcher
Matches tool names against patterns. Case-insensitive using Locale.ROOT.
// Exact match
HookMatcher exact = new HookMatcher("shell");
exact.matches("shell"); // true
exact.matches("Shell"); // true (case-insensitive)
// Glob wildcard
HookMatcher glob = new HookMatcher("file_*");
glob.matches("file_read"); // true
glob.matches("file_write"); // true
glob.matches("shell"); // false
// Match all tools
HookMatcher all = HookMatcher.ALL; // pattern "*"HookConfig
A single hook definition, typically parsed from YAML.
// Record: HookConfig(event, matcher, action)
HookConfig config = new HookConfig(
HookEvent.PRE_TOOL_USE,
new HookMatcher("file_*"),
HookAction.log("Tool ${tool} is about to execute")
);HookExecutor Interface
@FunctionalInterface
public interface HookExecutor {
void execute(HookAction action, Map<String, String> context);
}DeclarativeHookExecutor
Default executor that runs shell commands (with 30-second timeout) and logs messages. All execution is best-effort: failures are logged but never propagate.
Variable substitution replaces ${key} placeholders with values from the context map.
DeclarativeHookRegistry
Parses YAML hook definitions and registers them as runtime listeners on agents.
YAML schema:
hooks:
- event: preToolUse
matcher: "file_*"
action:
type: log
message: "Tool ${tool} is about to execute"
- event: postToolUse
matcher: "shell"
action:
type: command
command: "echo 'shell tool finished'"
- event: onStart
action:
type: log
message: "Agent started"Loading and applying hooks:
// Parse from YAML
InputStream yaml = new FileInputStream("hooks.yaml");
DeclarativeHookRegistry registry = DeclarativeHookRegistry.fromYaml(yaml);
// Parse with custom executor
DeclarativeHookRegistry registry = DeclarativeHookRegistry.fromYaml(yaml, customExecutor);
// Programmatic creation
DeclarativeHookRegistry registry = DeclarativeHookRegistry.of(hookConfigs);
// Apply to an agent (registers ToolCallListener + fires ON_START)
registry.applyTo(agent);Manual event firing:
// Fire a generic event
registry.fireEvent(HookEvent.ON_START, Map.of("agent", agentId));
// Fire a tool-specific event (checks HookMatcher against tool name)
registry.fireToolEvent(HookEvent.PRE_TOOL_USE, "shell",
Map.of("event", "preToolUse"));Querying hooks:
List<HookConfig> all = registry.getHooks();
List<HookConfig> preHooks = registry.getHooksForEvent(HookEvent.PRE_TOOL_USE);When applied to an agent via applyTo(), the registry creates a ToolCallListener that:
- Fires
PRE_TOOL_USEhooks inonToolCallStart - Fires
POST_TOOL_USEhooks inonToolCallComplete - Fires
ON_ERRORhooks whensuccessis false
Agent Factory
AgentFactory (com.tnsai.server.agent) creates agents for chat sessions with built-in tools and configurable LLM providers.
Built-in Tools
Every agent created by AgentFactory includes:
| Tool | Description |
|---|---|
ShellTool | Shell command execution with risk classification |
FileReadTool | Read file contents (auto-approved) |
FileWriteTool | Write file contents (requires approval) |
Creating Agents
// Setup
Supplier<LLMClient> llmSupplier = () -> new AnthropicClient("claude-sonnet-4-20250514");
AgentFactory factory = new AgentFactory(llmSupplier);
// Or with a custom working directory
AgentFactory factory = new AgentFactory(llmSupplier, Path.of("/project"));
// Simple creation (role name only)
Agent agent = factory.createAgent("agent-001", "assistant");
// With additional tools (e.g., MCP proxy tools)
Agent agent = factory.createAgent("agent-001", "developer", additionalTools);
// With provider and model override
Agent agent = factory.createAgent("agent-001", "developer",
"ollama", "glm-5:cloud", additionalTools);
// Full configuration with custom goal and domain
Agent agent = factory.createAgent("agent-001", "developer",
"ollama", "glm-5:cloud",
"Write clean, tested code for the TnsAI framework",
"tnsai-core",
additionalTools);
// Multi-role agent from RoleDef list
Agent agent = factory.createAgent("agent-001", "assistant",
"ollama", "glm-5:cloud", roleDefs, additionalTools);Built-in Role Goals
| Role | Goal |
|---|---|
assistant | Help users by answering questions. Use tools to read files, execute commands, and write code. |
developer | Write clean, well-tested code. Use shell and file tools to explore and implement. |
reviewer | Analyze code for bugs, style, and improvements. Read files and run tests. |
architect | Design systems and make technical decisions. Explore codebase structure. |
tester | Write tests and find edge cases. Use shell tools to run tests. |
| (other) | Perform the role of {name} effectively. Use available tools as needed. |
Provider Resolution
When provider is specified:
"ollama"-- createsOllamaClientwith the given model (default:"glm-5:cloud")- Any other value -- falls back to the default
LLMClientsupplier with a warning
Idle Shutdown Manager
IdleShutdownManager (com.tnsai.server.health) auto-shuts down the server after a configurable idle period when no active WebSocket connections exist (ADR-2: hybrid auto-daemon).
IdleShutdownManager manager = new IdleShutdownManager(
connectionManager, // WsConnectionManager instance
Duration.ofMinutes(30), // idle timeout
() -> server.stop() // shutdown action
);
manager.start();
// If no connections exist at start, schedules shutdown
// Called by WebSocket handlers
manager.onConnectionOpened(); // cancels pending shutdown
manager.onConnectionClosed(); // schedules shutdown if no connections remain
manager.stop(); // cleanupBehavior:
- On connection open: cancels any pending shutdown
- On connection close: if no active connections remain, schedules shutdown after timeout
- On timeout: re-checks for connections before shutting down (double-check safety)
- Uses a daemon thread (
tns-idle-shutdown) for the scheduler
RAG Pipeline
The RAG (Retrieval-Augmented Generation) system in com.tnsai.server.rag provides file indexing, code-aware chunking, and hybrid retrieval.
RagService
Orchestrates the full RAG pipeline for a single session. Thread-safe: indexing is serialized via a lock; reads (search) are concurrent.
RagService rag = new RagService();
// Index a directory
rag.indexDirectory(Path.of("/project/src"), progress ->
System.out.printf("Indexing: %d/%d - %s%n",
progress.indexedFiles(), progress.totalFiles(), progress.currentFile()));
// Search the knowledge base
List<SearchResult> results = rag.search("authentication logic", 5);
// Build a context-augmented prompt
String augmentedPrompt = rag.buildContextPrompt("How does auth work?", 3);
// Returns:
// [Relevant code context]
// --- file: src/main/java/Auth.java (lines 10-30) ---
// <chunk content>
//
// [User question]
// How does auth work?Document management:
// Add a document manually
String docId = rag.addDocument("Some text content",
Map.of("source", "manual", "tag", "notes"));
// Remove a document
boolean removed = rag.removeDocument(docId);
// List all managed documents
List<RagService.DocumentInfo> docs = rag.listDocuments();
// DocumentInfo(id, preview, contentLength, metadata)
// Get a specific document
Optional<Document> doc = rag.getDocument(docId);
// Get index status
IndexStatus status = rag.getStatus();
// IndexStatus(fileCount, chunkCount, lastIndexedAt, indexedPath)
// Clear everything
rag.clear();FileIndexer
Walks a directory tree and indexes supported source files into a KnowledgeBase.
Features:
- Respects
.gitignoreand.tnsignorepatterns - Skips binary files and known build/dependency directories
- SHA-256 content hashing for incremental re-indexing (unchanged files are skipped)
- Progress reporting via callback
Supported file extensions: java, ts, tsx, js, jsx, py, md, json, yml, yaml, xml, html, css, sh, sql, go, rs, rb, kt, scala, c, cpp, h
Skipped directories: .git, node_modules, build, dist, target, .idea, .vscode, .gradle, __pycache__, vendor, .next, out, coverage, .svn, .hg
Maximum file size: 512 KB
FileIndexer indexer = new FileIndexer();
FileIndexer.IndexResult result = indexer.index(
Path.of("/project"),
knowledgeBase,
bm25Stream,
progress -> System.out.println(progress.currentFile())
);
// IndexResult(fileCount, chunkCount)
// Force full re-index on next run
indexer.clearHashes();CodeChunker
Splits source code files into semantically meaningful chunks for RAG indexing. Each chunk is a Document with metadata: file, startLine, endLine, language.
Chunking strategies by language:
| Language | Strategy |
|---|---|
| Java, Kotlin, Scala | Class and method boundary detection via regex |
| TypeScript, JavaScript | Function, class, and arrow-function boundary detection |
| Markdown | Heading boundary detection |
| Everything else | Fixed-size line groups (max 100 lines) |
Files under 100 lines are kept as a single chunk.
// Chunk a file
List<Document> chunks = CodeChunker.chunk(
Path.of("src/Auth.java"), // file path (metadata)
fileContent, // file content string
"java" // language key
);
// Detect language from filename
String lang = CodeChunker.detectLanguage("Auth.java"); // "java"
String lang = CodeChunker.detectLanguage("app.tsx"); // "tsx"
String lang = CodeChunker.detectLanguage("README.md"); // "md"
String lang = CodeChunker.detectLanguage("unknown.xyz"); // "text"Hybrid Retrieval
RagService uses a HybridRetriever that combines two retrieval streams:
- BM25Stream (weight 0.6) -- keyword-based BM25 scoring
- KnowledgeBaseStream (weight 0.4) -- vector similarity from the
KnowledgeBase
Results are merged and ranked by weighted score.
Cross-References
- WebSocket Protocol -- WebSocket v1 protocol for client communication
- Tool Approval -- tool approval flow via WebSocket
- RAG Pipeline -- RAG pipeline overview
- Observability -- evaluation hooks and tracing
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.
MCP
Model Context Protocol — client, server, transports, registry.