Skip to content
tnsaijava agent framework

Actions vs Tools

Three names sit next to each other in TnsAI. They are not interchangeable.

SurfaceWhat it isWho invokes it
@ActionSpecA Role method the framework routesActionExecutor (and the LLM, unless excludeFromLLM)
@ToolA POJO method exposed as an LLM functionThe model, through ToolMethodDispatcher
BuiltInToolAn enum entry that instantiates a shipped toolkitYou, at agent construction

Use actions for work the Role owns (local code, HTTP, MCP, or an LLM turn). Use tools for functions the model may call during chat or during an ActionType.LLM action. Use BuiltInTool only as the compile-safe handle for a POJO that already lives in tnsai-tools.

@ActionSpec

Put this on a Role method. Discovery scans the Role at initialization and ActionExecutor dispatches by ActionType (LOCAL, WEB_SERVICE, LLM, MCP_TOOL). There is no fifth type.

@ActionSpec(type = ActionType.LOCAL, description = "Look up an order")
public Order findOrder(String orderId) {
    return orders.get(orderId);
}

An LLM action does not register tools. It runs an LLM turn that may call tools already registered on the Agent:

@ActionSpec(
    type = ActionType.LLM,
    description = "Research a topic with the agent's tools")
public String research(String topic) {
    return "Research: " + topic;
}

excludeFromLLM = true keeps the action callable from code and hides it from function-calling.

Do not put @ActionSpec on a toolkit POJO. That is not how toolkits are discovered.

@Tool

Put this on a public method of a POJO. The method body runs. Register the instance with AgentBuilder.toolPojos(...). There is no Tool interface and no Role.tools() hook on current main.

public final class OrderTools {

    @Tool(name = "order_lookup", description = "Fetch one order by id")
    public Order lookup(@ToolParam(description = "Order id") String id) {
        return store.get(id);
    }
}

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(orderRole)
    .toolPojos(new OrderTools())
    .build();

Each @Tool method becomes one JSON-Schema function. The model picks a name; ToolMethodDispatcher invokes the method.

BuiltInTool

One enum constant = one shipped POJO class = that class's @Tool methods. tnsai-core stores the FQCN as a string so it does not depend on tnsai-tools at compile time. instantiate() uses Class.forName and throws BuiltInToolInstantiationException if the artifact is missing.

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(researchRole)
    .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS, BuiltInTool.PDF_TOOLS)
    .toolPojos(new OrderTools())
    .build();

The string BuiltInTool.WEB_SEARCH_TOOLS.getToolName() is a catalog key. It is not a substitute for @ActionSpec and it does not open an MCP server.

Which one

  • Role-owned step, typed dispatch, contracts, or MCP/HTTP@ActionSpec.
  • Model-callable helper the Role does not own@Tool on a POJO + toolPojos.
  • Shipped toolkit already in tnsai-toolsBuiltInTool + builtInTools.
  • ActionType.LLM plus web search@ActionSpec(type = LLM) and .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS). One does not imply the other.