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 intnsai-tools. See the catalog.toolPojos(Object...)adds your own classes with@Toolmethods. See Custom tools.dynamicTool(DynamicToolMethod)adds a tool whose name and schema are known only at runtime.
Register toolkits
build() requires principal and authorityScope; liabilitySink is optional.
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.
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.
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: write
@Toolmethods. - Tool catalog: the shipped toolkits.
- Idempotency: tools that must not run twice.
- MCP: tools from MCP servers.