Skip to content
tnsaijava agent framework

Tool Integration

A "tool" in TnsAI is a Java method annotated @Tool. Methods are grouped on POJO classes (toolkits); the framework discovers them reflectively at agent build time and exposes each as a function the LLM can call. Two registration paths share the same underlying ToolMethodRegistry:

  • AgentBuilder.builtInTools(BuiltInTool...) — the shipped toolkits in tnsai-tools
  • AgentBuilder.toolPojos(Object...) — your own annotated POJOs

For runtime-defined tools (e.g. MCP proxies), see dynamicTool(...) further down.

Registering Tools

AgentBuilder.build() requires the accountability trio (TNS-298). Later snippets on this page omit those three calls.

package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.enums.BuiltInTool;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.roles.Role;

public final class ToolsRegistrationBuilderExample {
    private ToolsRegistrationBuilderExample() {}

    public static Agent build(
        LLMClient llm,
        Role role,
        Object myDomainTools,
        Object myAnalyticsTools,
        AgentPrincipal principal,
        LiabilitySink sink,
        AuthorityScope scope
    ) {
        return AgentBuilder.create()
            .llm(llm)
            .role(role)
            .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS, BuiltInTool.UTILITY_TOOLS)
            .toolPojos(myDomainTools, myAnalyticsTools)
            .principal(principal)
            .liabilitySink(sink)
            .authorityScope(scope)
            .build();
    }
}

Both calls accumulate into a single ToolMethodRegistry. Duplicate @Tool(name=...) values across the entire registration set fail fast at build() time.

Defining a @Tool method

import com.tnsai.annotations.Tool;
import com.tnsai.annotations.ToolParam;

public class MyDomainTools {

    @Tool(name = "find_customer", description = "Look up a customer by email")
    public Customer findCustomer(
        @ToolParam(description = "The customer's email address") String email
    ) {
        return customerRepo.findByEmail(email);
    }
}

The method name (or explicit @Tool(name = ...) if you prefer it different from the Java identifier) is what the LLM calls. @ToolParam descriptions populate the JSON-Schema sent to the model — write them like API parameter docs.

TnsAI 0.14.0 (TnsAI@a070d674, TAN-3411) requires a real source name or @ToolParam(name=...). A synthetic arg0 without an explicit name throws IllegalStateException at registration instead of advertising garbage schema names. The framework parent POM already passes -parameters. Consumer POJOs must do the same or set name. This ships in Maven Central 0.14.1.

For a deeper walk-through, see Custom Tools. For the shipped catalog, see Catalog.

Runtime-Defined Tools

When a tool's identity is only known at runtime — typically an MCP proxy fronting a remote server's catalog — register it with dynamicTool(...) instead of an annotated POJO:

import com.tnsai.tools.method.DynamicToolMethod;

DynamicToolMethod proxy = DynamicToolMethod.builder()
    .name("remote_search")
    .description("Search the remote knowledge base")
    .parameter("query", "string", "Search term")
    .handler(args -> remoteClient.search((String) args.get("query")))
    .build();

AgentBuilder.create()
    .llm(llm)
    .role(role)
    .dynamicTool(proxy)
    .principal(principal)
    .liabilitySink(sink)
    .authorityScope(scope)
    .build();

DynamicToolMethod and POJO @Tool methods share the same registry and dispatcher; the LLM can't distinguish them.

Tool Call Filters

Sometimes you want to restrict what an agent can do at runtime. Tool call filters intercept every tool call before it executes and decide whether to allow, block, or redirect it. Useful for enforcing safety policies, preventing destructive operations, or guiding the LLM toward better tool usage.

agent.setToolCallFilter((toolName, arguments) -> {
    // Block dangerous tools
    if (toolName.equals("file_write")) return false;

    // Allow everything else
    return true;
});

For more nuanced control, use ToolCallAction:

agent.setToolCallFilter((toolName, arguments) -> {
    if (toolName.equals("sql_query")) {
        String query = (String) arguments.get("query");
        if (query.toUpperCase(Locale.ROOT).contains("DROP")) {
            return ToolCallAction.block();
        }
        if (query.toUpperCase(Locale.ROOT).contains("DELETE")) {
            return ToolCallAction.guide("Use soft deletes instead of DELETE statements");
        }
    }
    return ToolCallAction.allow();
});
ActionBehavior
ToolCallAction.allow()Proceed as-is
ToolCallAction.block()Reject the call
ToolCallAction.guide(message)Provide corrective feedback to the LLM
ToolCallAction.fallback(suggestion)Suggest an alternative tool or approach

Tool Call Listeners

Tool call listeners observe execution without affecting it. Callbacks fire when a tool call starts and completes, exposing the tool name, arguments, result, and latency. The right place to add logging, latency metrics, or debugging dashboards.

agent.setToolCallListener(new ToolCallListener() {
    @Override
    public void onToolCallStart(String toolName, Map<String, Object> arguments) {
        log.info("Calling tool: {} with args: {}", toolName, arguments);
    }

    @Override
    public void onToolCallComplete(String toolName, Object result, boolean success, long latencyMs) {
        log.info("Tool {} completed in {}ms (success={})", toolName, latencyMs, success);
    }
});