Actions vs Tools
Three names sit next to each other in TnsAI. They are not interchangeable.
| Surface | What it is | Who invokes it |
|---|---|---|
@ActionSpec | A Role method the framework routes | ActionExecutor (and the LLM, unless excludeFromLLM) |
@Tool | A POJO method exposed as an LLM function | The model, through ToolMethodDispatcher |
BuiltInTool | An enum entry that instantiates a shipped toolkit | You, 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 →
@Toolon a POJO +toolPojos. - Shipped toolkit already in
tnsai-tools→BuiltInTool+builtInTools. ActionType.LLMplus web search →@ActionSpec(type = LLM)and.builtInTools(BuiltInTool.WEB_SEARCH_TOOLS). One does not imply the other.
Related
Tools — Advanced
The function-shape POJO model deliberately keeps the tool surface small: a method, an annotation, a registry. Most "advanced" features that older docs covered (manifest generators, contract validators, security enforcers, parameter validators, retry/cache wrappers) were retired together with the legacy Tool interface in v0.6.0 / v0.7.0. Cross-cutting concerns now live one layer up — on the @ActionSpec annotation, on the agent's setToolCallFilter / setToolCallListener hooks, or on the dispatcher itself.
Docling
Shipped in 0.14.0 (TAN-3648, framework PR #153, commit eeb16b48). DoclingTools is the 63rd POJO toolkit and contributes one @Tool method.