Skip to content
tnsaijava agent framework

Actions vs Tools

TnsAI has three overlapping extension points. They are not interchangeable.

PrimitiveWhere it livesWho calls itSchema for the LLMTypical trigger
@ActionSpec on a Role methodRole classActionExecutor (executeAction / role routing)Not a function schema. An LLM action can use registered tools; the action method itself is not oneDeterministic, typed work the agent (or your code) invokes by name
@Tool on a POJO methodAny POJO you registerToolMethodDispatcher during an LLM tool-call loopYes — method name + @ToolParam become the JSON schemaThe model chooses a function at generation time
BuiltInTool enum constanttnsai-tools catalogSame dispatcher, after AgentBuilder.builtInTools(...) instantiates the POJOYes — every @Tool on the backing classYou want a shipped toolkit instead of writing one

BuiltInTool is not a third runtime. It is a compile-safe index of shipped @Tool POJOs (BuiltInTool.java). A ServiceLoader replacement is tracked as TAN-2977 — do not treat the enum as the forever registration API.

Decision

Use @ActionSpec when the call must be typed, deterministic, and routed by ActionType (LOCAL, WEB_SERVICE, LLM, MCP_TOOL). Approvals, contracts, and resilience hang off this path. See Action System.

Use @Tool when the LLM should see a function and pick it. Register the POJO with AgentBuilder.toolPojos(...). See Custom Tools.

Use BuiltInTool when that function already ships in tnsai-tools. Register the enum constant; do not re-wrap the POJO in an action. See Catalog.

Same domain, three primitives

Forecast lookup as a local action, a custom tool, and a shipped toolkit. AgentBuilder.build() still needs the accountability trio; the snippets below omit it.

1. Local @ActionSpec — your code calls it

public class WeatherRole extends Role {
    @Override public RoleIdentity getIdentity() {
        return new RoleIdentity("weather", "Looks up forecasts", "ops");
    }

    @ActionSpec(type = ActionType.LOCAL, description = "Return a stored forecast")
    public String forecast(String city) {
        return weatherService.forecast(city);
    }
}

ActionResponse response = agent.executeAction(
    ActionRequest.of("forecast", Map.of("city", "Istanbul")));

The LLM does not receive a forecast function schema from this method. To hide an action from any LLM-facing list, set excludeFromLLM = true.

2. @Tool POJO — the model calls it

public class WeatherTools {
    @Tool(name = "weather_forecast", description = "Forecast for a city")
    public String weatherForecast(
        @ToolParam(description = "City name, e.g. Istanbul") String city
    ) {
        return weatherService.forecast(city);
    }
}

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .toolPojos(new WeatherTools())
    .principal(principal)
    .liabilitySink(sink)
    .authorityScope(scope)
    .build();

The dispatcher exposes weather_forecast as a function. Do not put API keys or other secrets in @Tool / @ToolParam descriptions or names — they are sent to the model. Keep credentials in environment variables (see Custom Tools). Per-call policy belongs on setToolCallFilter / before-hooks (TAN-2886), not in the schema.

3. BuiltInTool — shipped catalog, same dispatcher

There is no BuiltInTool.HTTP_TOOLS. For "let the model look this up" use a shipped search toolkit:

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS)
    .principal(principal)
    .liabilitySink(sink)
    .authorityScope(scope)
    .build();

WEB_SEARCH_TOOLS instantiates com.tnsai.tools.search.WebSearchTools and registers its @Tool methods (brave_search, duckduckgo, …). Same ToolMethodDispatcher as a custom POJO.

Do not

  • Do not wrap a BuiltInTool POJO in an @ActionSpec just to "expose" it. Register the enum (or toolPojos) and let the dispatcher own the schema.
  • Do not put an @ActionSpec on a method and expect the LLM to call it as a function. That is @Tool.
  • Do not put secrets in tool schemas.
  • Do not invent BuiltInTool.HTTP_TOOLS — it is not on the 0.13.0 enum.