# 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.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ToolsRegistrationBuilderExample.java -->
```java
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

```java
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](custom-tools.md). For the shipped catalog, see [Catalog](catalog.md).

## 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:

```java
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.

```java
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`:

```java
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();
});
```

| Action | Behavior |
|--------|----------|
| `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.

```java
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);
    }
});
```
