# Custom tools

A custom tool is a public method annotated `@Tool` on an ordinary Java class. There is no base class or interface to implement: pass an instance to `AgentBuilder.toolPojos(...)` and each `@Tool` method becomes a function the model can call.

For the toolkits that ship with TnsAI, see the [catalog](catalog.md). To choose between an `@ActionSpec` action, a `@Tool` method and a `BuiltInTool`, see [Actions and tools](../agents/actions-and-tools.md).

## A minimal toolkit

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

public class CalculatorTools {

    @Tool(name = "calculator", description = "Evaluate an arithmetic expression")
    public double calculator(
        @ToolParam(description = "Expression like '2 + 2 * (3 - 1)'") String expression
    ) {
        return new ExpressionParser().parse(expression).evaluate();
    }
}
```

Register it. `build()` requires `principal` and `authorityScope`; `liabilitySink` is optional. Later snippets leave these calls out.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ToolsCustomBuilderExample.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.identity.AgentPrincipal;
import com.tnsai.llm.providers.OpenAIClient;
import com.tnsai.roles.Role;

public final class ToolsCustomBuilderExample {
    private ToolsCustomBuilderExample() {}

    public static Agent build(
        Role myRole,
        Object calculatorTools,
        AgentPrincipal principal,
        LiabilitySink sink,
        AuthorityScope scope
    ) {
        return AgentBuilder.create()
            .llm(new OpenAIClient("gpt-4o"))
            .role(myRole)
            .toolPojos(calculatorTools)
            .principal(principal)
            .liabilitySink(sink)
            .authorityScope(scope)
            .build();
    }
}
```

The model calls the tool by its `@Tool` name, `calculator` here. When `name` is empty, the method name is used. `description` is required. `@ToolParam` descriptions go into the JSON Schema sent to the model, so write them as you would document an API parameter.

Parameter names come from `@ToolParam(name = ...)` or from the compiled method. Compile your toolkits with `-parameters`, or name each parameter. Otherwise registration fails with `IllegalStateException`.

Only public, non-static methods are registered.

## Several tools on one class

Each `@Tool` method is a separate tool, and the model picks one per call.

```java
public class WeatherTools {

    @Tool(name = "weather_current", description = "Current weather for a city")
    public WeatherSnapshot weatherCurrent(
        @ToolParam(description = "City name, e.g. 'Istanbul'") String city
    ) {
        return weatherClient.getCurrent(city);
    }

    @Tool(name = "weather_forecast", description = "5-day forecast for a city")
    public List<DailyForecast> weatherForecast(
        @ToolParam(description = "City name") String city,
        @ToolParam(description = "Number of days, 1-5") int days
    ) {
        return weatherClient.getForecast(city, days);
    }
}
```

`toolPojos(new WeatherTools())` registers both. The model receives the return value as text, through its `toString()`. Return a `String`, or a type such as a record whose `toString()` reads well.

## Credentials

Read API keys from the environment when the tool runs, not in the constructor. `BuiltInTool` creates toolkits through their no-arg constructors, and this keeps your toolkit usable the same way.

```java
public class WeatherTools {

    private static String requireApiKey() {
        String key = System.getenv("WEATHER_API_KEY");
        if (key == null || key.isBlank()) {
            throw new IllegalStateException("WEATHER_API_KEY environment variable is required");
        }
        return key;
    }

    @Tool(name = "weather_current", description = "Current weather for a city")
    public WeatherSnapshot weatherCurrent(@ToolParam(description = "City") String city) {
        String key = requireApiKey();
        // ...
    }
}
```

## Mixing with shipped toolkits

`toolPojos(...)`, `builtInTools(...)` and `dynamicTool(...)` fill the same registry. Tool names must be unique per agent; a duplicate fails the build with `IllegalArgumentException`. See [Register tools](register.md).

## Per-tool deadlines

```java
public class SearchTools {

    @Tool(description = "Search the web for a query", timeoutMs = 5_000)
    public String search(@ToolParam(description = "Query") String query) {
        return httpClient.get(query);
    }
}
```

A positive `timeoutMs` is one wall-clock budget for the call, including idempotency handling and the method body. When it runs out, the call fails with `ToolTimeoutException`, which is retryable. The default, `0`, adds no deadline of its own.

The worker is interrupted when the deadline passes. A method that ignores interruption can keep running. `timeoutMs` applies only to `@Tool` methods: a `DynamicToolMethod` has no deadline.

## Related

- [Register tools](register.md): runtime-defined tools, filters and listeners.
- [Idempotency](idempotency.md): tools that must not run twice.
- [Actions](../agents/actions.md): per-action model settings such as `llmSystemPrompt`.
