TnsAI

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 ActionExecutionException

Tool 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:

ValueExecutorDescription
LOCALReflectionDirect Java method invocation on the Role
WEB_SERVICEWebServiceExecutorHTTP REST API calls
LLMLLMRoleExecutorSingle-shot LLM call; tool dispatch via the agent's ToolMethodDispatcher
MCP_TOOLMcpToolExecutorModel 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

  1. Validation -- null checks on action, role, parameters
  2. Approval check -- if the action method has @ApprovalRequired, verifies _approval_token is present in parameters
  3. Routing -- LOCAL actions are invoked via reflection; others delegate to TypedActionExecutor
  4. Error wrapping -- all exceptions become ActionExecutionException with 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: BEARER and BASIC via environment variables
  • Custom headers via @Header annotations
  • 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 fieldEffect
llmSystemPromptPrepended as the system message of the chat request, overriding the LLM client's default for this action
llmTemperatureWhen >= 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:

MethodDescription
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

SourceTarget Types
Stringint, long, double, float, boolean, Enum
Numberint, 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:

MethodDescription
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());
}
  • Roles -- defining roles with @ActionSpec annotations
  • Capabilities -- reusable body-less @ActionSpec contracts via @Capability interfaces
  • Tools -- registering POJO toolkits the LLM can call
  • Advanced Tools -- filters, listeners, and dispatcher introspection
  • SPI Reference -- SPI interfaces for cross-module extensibility

On this page