Action System
The action system is the execution backbone of TnsAI. When an LLM decides to call a function, or an agent needs to perform work, the request flows through ActionExecutor, which routes it to the appropriate executor based on the action's ActionType.
Architecture Overview
Agent.executeAction(name, params)
|
v
ActionExecutor.execute(action, role, params, context)
|
+-- 1. Validate inputs
+-- 2. Check @ApprovalRequired
+-- 3. Route by ActionType:
| LOCAL -> Reflection invocation on Role
| WEB_SERVICE -> WebServiceExecutor (HTTP)
| LLM -> LLMRoleExecutor (single-shot LLM call)
| MCP_TOOL -> McpToolExecutor (MCP protocol)
+-- 4. Return result or wrap in ActionExecutionExceptionTool dispatch — when the LLM emits a tool call during an LLM action — is handled separately by the agent's ToolMethodDispatcher, which is built from every POJO and dynamic tool registered with AgentBuilder.
ActionType Enum
com.tnsai.enums.ActionType defines the four execution methods:
| Value | Executor | Description |
|---|---|---|
LOCAL | Reflection | Direct Java method invocation on the Role |
WEB_SERVICE | WebServiceExecutor | HTTP REST API calls |
LLM | LLMRoleExecutor | Single-shot LLM call; tool dispatch via the agent's ToolMethodDispatcher |
MCP_TOOL | McpToolExecutor | Model Context Protocol server calls |
ActionExecutor
com.tnsai.actions.ActionExecutor is the central, thread-safe dispatcher.
Construction
// Built by AgentBuilder using the agent's tool registry
ActionExecutor executor = new ActionExecutor(toolMethodDispatcher);Default executors are registered automatically: WebServiceExecutor for WEB_SERVICE, LLMRoleExecutor for LLM, and McpToolExecutor for MCP_TOOL (if tnsai-mcp is on classpath). The ToolMethodDispatcher is built from AgentBuilder.builtInTools(...), .toolPojos(...), and .dynamicTool(...) registrations.
Execution Flow
- Validation -- null checks on action, role, parameters
- Approval check -- if the action method has
@ApprovalRequired, verifies_approval_tokenis present in parameters - Routing --
LOCALactions are invoked via reflection; others delegate toTypedActionExecutor - Error wrapping -- all exceptions become
ActionExecutionExceptionwith a category (PARAMETER,INVOCATION,NETWORK,UNKNOWN)
Approval-Required Actions
// In a Role class
@ApprovalRequired
@ActionSpec(description = "Deploy to production")
public String deploy(String target) { ... }
// Calling with approval token
Map<String, Object> params = Map.of(
"target", "production",
"_approval_token", approvalService.getToken()
);
agent.executeAction("deploy", params);If the token is missing, ApprovalRequiredException is thrown.
TypedActionExecutor Interface
com.tnsai.actions.executors.TypedActionExecutor is the extension point for custom execution strategies.
public interface TypedActionExecutor {
Object execute(
ActionMetadata action,
Role role,
Map<String, Object> parameters,
Map<String, Object> context
);
}The context map provides runtime data: "llm" (LLMClient), "agent" (Agent reference), "mcpToolName" (for MCP routing).
Executor Types
WebServiceExecutor
Handles WEB_SERVICE actions by making HTTP calls using OkHttp.
Features:
- URL template variables:
{city},{id}in endpoints are replaced from parameters - Parameter types:
QUERY(URL params),PATH(URL path segments),BODY(JSON payload) - Authentication:
BEARERandBASICvia environment variables - Custom headers via
@Headerannotations - Configurable per-action timeout
@ActionSpec(
type = ActionType.WEB_SERVICE,
endpoint = "https://api.weather.com/v1/forecast/{city}",
httpMethod = HttpMethod.GET,
paramType = ParamType.QUERY,
auth = AuthType.BEARER,
authToken = "WEATHER_API_KEY",
timeout = 5000
)
@Header(key = "Accept", value = "application/json")
public Object getWeather(String city) { return null; }LLMRoleExecutor
Handles LLM actions with a single-shot LLM call. The action's prompt comes from the method body's return value (the convention is to return a String describing what the LLM should do); the executor sends that prompt to the agent's LLM client and returns the raw response.
Tool dispatch is not done by this executor. If the LLM emits a tool call in its response, dispatch flows through the agent's ToolMethodDispatcher, which is built once from the AgentBuilder.builtInTools(...) / .toolPojos(...) / .dynamicTool(...) registrations and shared across every LLM action.
Per-action overrides:
@ActionSpec field | Effect |
|---|---|
llmSystemPrompt | Prepended as the system message of the chat request, overriding the LLM client's default for this action |
llmTemperature | When >= 0, sets the chat temperature; -1.0f (default) means "fall back to the LLM client's default" |
McpToolExecutor
Handles MCP_TOOL actions by connecting to remote MCP servers.
Features:
- Automatic tool discovery via
tools/list - Connection caching per endpoint
- API key support via environment variables
- Reflection-based MCP client creation (no compile-time dependency on tnsai-mcp)
@ActionSpec(
type = ActionType.MCP_TOOL,
serverUrl = "https://mcp.api.coingecko.com/mcp",
description = "Access cryptocurrency data"
)
public String cryptoData(String query) { return null; }Key methods:
| Method | Description |
|---|---|
discoverTools(String endpoint, String apiKeyEnv) | Discover tools from MCP server |
getEndpointForTool(String toolName) | Find which endpoint handles a tool |
close() | Disconnect all cached MCP clients |
ActionContract
com.tnsai.actions.contracts.ActionContract provides optional pre/post condition validation for roles.
public interface ActionContract {
default void validatePreConditions(ActionMetadata action, Map<String, Object> parameters)
throws ValidationException { }
default void validatePostConditions(ActionMetadata action, Object result)
throws ValidationException { }
default void validateInvariants() throws ValidationException { }
}A role implements this interface to enforce contracts:
public class OrderRole extends Role implements ActionContract {
@Override
public void validatePreConditions(ActionMetadata action, Map<String, Object> params)
throws ValidationException {
if ("placeOrder".equals(action.getName())) {
if (!params.containsKey("items")) {
throw new ValidationException("items parameter required");
}
}
}
}TypeConverter
com.tnsai.actions.TypeConverter handles automatic parameter type conversion in action invocations.
Supported Conversions
| Source | Target Types |
|---|---|
String | int, long, double, float, boolean, Enum |
Number | int, long, double, float |
Map<String, Object> | Any POJO/record (via Jackson) |
// String to int
Object result = TypeConverter.convert("42", int.class); // 42
// Number to int
Object result = TypeConverter.convert(42L, int.class); // 42
// Map to POJO
record UserDto(String name, int age) {}
Map<String, Object> params = Map.of("name", "Alice", "age", 30);
UserDto user = TypeConverter.convertMapToPojo(params, UserDto.class);Utility methods:
| Method | Description |
|---|---|
convert(Object value, Class<?> targetType) | Convert a value to the target type |
convertMapToPojo(Map, Class<T>) | Convert a map to a POJO via Jackson |
isPrimitiveOrWrapper(Class<?>) | Check if type is primitive or wrapper |
isSimpleType(Class<?>) | Check if type does not need POJO conversion |
Enum conversion is case-insensitive: TypeConverter.convert("get", HttpMethod.class) matches HttpMethod.GET.
ActionRequest and ActionResponse
Typed records that replace raw Map<String, Object> and untyped Object returns.
ActionRequest
com.tnsai.actions.model.ActionRequest -- immutable request record.
// With parameters
ActionRequest request = ActionRequest.of("searchWeb", Map.of(
"query", "Java frameworks",
"maxResults", 10
));
// Without parameters
ActionRequest request = ActionRequest.of("getStatus");Fields: actionName (required, non-blank), parameters (never null, defensively copied).
ActionResponse
com.tnsai.actions.model.ActionResponse -- immutable response record.
// Success
ActionResponse response = ActionResponse.success(resultData);
// Failure
ActionResponse response = ActionResponse.failure("Connection timeout");
// Failure with partial result
ActionResponse response = ActionResponse.failure("Partial data received", partialData);Fields: value (result object), success (boolean), error (String, null on success).
Usage with Agent
ActionRequest request = ActionRequest.of("searchWeb", Map.of("query", "TnsAI"));
ActionResponse response = agent.executeAction(request);
if (response.success()) {
Object data = response.value();
} else {
logger.error("Failed: {}", response.error());
}Related Documentation
- Roles -- defining roles with
@ActionSpecannotations - Capabilities -- reusable body-less
@ActionSpeccontracts via@Capabilityinterfaces - Tools -- registering POJO toolkits the LLM can call
- Advanced Tools -- filters, listeners, and dispatcher introspection
- SPI Reference -- SPI interfaces for cross-module extensibility
Roles
A Role defines what an agent can do. Each role has an identity (name, goal, domain), a set of responsibilities, and discoverable actions. Roles generate the system prompt that instructs the LLM. Actions are methods annotated with @ActionSpec — they are discovered at runtime via reflection and routed to one of four executor types.
Capabilities
Capabilities are reusable, body-less action contracts. A @Capability interface carries one or more @ActionSpec-annotated methods that describe what the capability does; the framework dispatches the call at runtime. A role gains the capability by implements-ing the interface — without writing any method bodies.