# Register tools

A tool is a method the model can call. TnsAI collects every tool an agent has into one registry, and the agent's dispatcher runs each call the model makes. There are three ways in:

- `builtInTools(BuiltInTool...)` adds toolkits shipped in `tnsai-tools`. See the [catalog](catalog.md).
- `toolPojos(Object...)` adds your own classes with `@Tool` methods. See [Custom tools](custom.md).
- `dynamicTool(DynamicToolMethod)` adds a tool whose name and schema are known only at runtime.

## Register toolkits

`build()` requires `principal` and `authorityScope`; `liabilitySink` is optional.

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

`builtInTools` throws `BuiltInToolInstantiationException` when `tnsai-tools` is not on the classpath.

Only public, non-static `@Tool` methods are registered. A tool's name is `@Tool(name = ...)`, or the method name when `name` is empty. Names must be unique per agent across all three paths: a duplicate fails the build with `IllegalArgumentException`.

## Runtime-defined tools

Use `DynamicToolMethod` when a tool's name and schema come from somewhere else, such as a remote catalog. The handler receives the model's arguments as a `Map`. Name and description must not be blank.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ToolsDynamicExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.tools.method.DynamicToolMethod;
import com.tnsai.tools.method.ToolMethod;
import com.tnsai.tools.method.ToolMethodDispatcher;

import java.util.List;
import java.util.Map;
import java.util.function.Function;

public final class ToolsDynamicExample {
    private ToolsDynamicExample() {}

    // A tool whose name and schema are known only at runtime.
    static AgentBuilder withRemoteSearch(AgentBuilder builder, Function<String, Object> remoteSearch) {
        DynamicToolMethod search = DynamicToolMethod.builder("remote_search")
            .description("Search the remote knowledge base")
            .idempotent(true)
            .parametersSchema(Map.of(
                "type", "object",
                "properties", Map.of(
                    "query", Map.of("type", "string", "description", "Search terms")),
                "required", List.of("query")))
            .handler(args -> remoteSearch.apply((String) args.get("query")))
            .build();
        return builder.dynamicTool(search);
    }

    // List every tool the model can call.
    static void list(Agent agent) {
        ToolMethodDispatcher dispatcher = agent.getAgentActionExecutor().getToolDispatcher();
        for (ToolMethod tool : dispatcher.registry().all()) {
            System.out.println(tool.name() + ": " + tool.description());
        }
    }

    // Call a tool from code, outside the model loop.
    static Object call(Agent agent, String path) {
        ToolMethodDispatcher dispatcher = agent.getAgentActionExecutor().getToolDispatcher();
        if (dispatcher.lookup("csv_summary") == null) {
            throw new IllegalStateException("csv_summary is not registered");
        }
        return dispatcher.dispatch("csv_summary", Map.of("path", path));
    }
}
```

`getToolDispatcher().registry().all()` lists every registered tool. `lookup(name)` returns the tool's metadata, or `null` when no tool has that name.

`dispatch(name, arguments)` is the entry point the model's tool calls use, with the same access checks. It throws `IllegalArgumentException` for an unknown name. A checked exception from the tool is wrapped in `IllegalStateException`; an unchecked one propagates as is.

## Tool call filters

A filter decides, before each tool call, whether it runs. An agent has one filter: set it on the builder with `toolCallFilter(...)` or later with `agent.setToolCallFilter(...)`, which replaces the previous one.

`ToolCallFilter` is a functional interface over `isAllowed(toolName, arguments)`, so a lambda can allow or block. To guide the model or point it at another tool, override `evaluate`.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ToolsFilterExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.agents.execution.ToolCallAction;
import com.tnsai.agents.execution.ToolCallFilter;
import com.tnsai.agents.execution.ToolCallListener;

import java.util.List;
import java.util.Map;
import java.util.Set;

public final class ToolsFilterExample {
    private ToolsFilterExample() {}

    // allow or block only: a lambda for isAllowed is enough.
    static void readOnly(Agent agent) {
        agent.setToolCallFilter((toolName, arguments) -> !toolName.equals("file_write"));
    }

    // guide and fallback: override evaluate.
    static void steer(Agent agent) {
        agent.setToolCallFilter(new ToolCallFilter() {
            @Override
            public boolean isAllowed(String toolName, Map<String, Object> arguments) {
                return evaluate(toolName, arguments) instanceof ToolCallAction.Allow;
            }

            @Override
            public ToolCallAction evaluate(String toolName, Map<String, Object> arguments) {
                if (Set.of("sql_query", "file_write").contains(toolName)) {
                    return ToolCallAction.block();
                }
                if (toolName.equals("brave_search") && !arguments.containsKey("query")) {
                    return ToolCallAction.guide("Pass the search terms in 'query'.");
                }
                if (toolName.equals("serpapi")) {
                    return ToolCallAction.fallback("Use brave_search instead.");
                }
                return ToolCallAction.allow();
            }
        });
    }

    // One listener per agent. Fan out to several.
    static final class CompositeListener implements ToolCallListener {
        private final List<ToolCallListener> delegates;

        CompositeListener(ToolCallListener... delegates) {
            this.delegates = List.of(delegates);
        }

        @Override
        public void onToolCallStart(String toolName, Map<String, Object> arguments) {
            delegates.forEach(l -> l.onToolCallStart(toolName, arguments));
        }

        @Override
        public void onToolCallComplete(String toolName, Object result, boolean success, long latencyMs) {
            delegates.forEach(l -> l.onToolCallComplete(toolName, result, success, latencyMs));
        }
    }

    static void observe(Agent agent, ToolCallListener metrics, ToolCallListener audit) {
        agent.setToolCallListener(new CompositeListener(metrics, audit));
    }
}
```

| Action | What the model sees |
|---|---|
| `ToolCallAction.allow()` | The tool result |
| `ToolCallAction.block()` | `DENIED: …`; the tool does not run |
| `ToolCallAction.guide(feedback)` | `GUIDANCE: <feedback>` and a request to adjust the parameters and retry |
| `ToolCallAction.fallback(suggestion)` | `REDIRECT: <suggestion>` and a request to use the alternative |

The filter runs before the agent's authorization checks and before the listener's start event, so a call it stops never reaches either.

## Tool call listeners

A listener observes tool calls without changing them. `onToolCallStart(toolName, arguments)` fires before the tool runs, and `onToolCallComplete(toolName, result, success, latencyMs)` after it. An agent has one listener; `setToolCallListener` replaces it, so fan out with a composite as above.

## Related

- [Custom tools](custom.md): write `@Tool` methods.
- [Tool catalog](catalog.md): the shipped toolkits.
- [Idempotency](idempotency.md): tools that must not run twice.
- [MCP](../mcp/index.md): tools from MCP servers.
