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

| 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

```java
// 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

```java
// 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.

```java
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 nested `@Header` on `@WebService.headers`
- Configurable per-action timeout

```java
@ActionSpec(
    type = ActionType.WEB_SERVICE,
    webService = @WebService(
        endpoint = "https://api.weather.com/v1/forecast/{city}",
        method = HttpMethod.GET,
        paramType = ParamType.QUERY,
        auth = AuthType.BEARER,
        authTokenEnv = "WEATHER_API_KEY",
        timeout = 5000,
        headers = {
            @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.

A missing client is a **failure**, not a fallback. If the action context has no `"llm"` `LLMClient`, the executor throws `ActionExecutionException` (`ErrorCategory.VALIDATION`, not retryable). Returning `null` used to be logged as a fallback; `@Fallback` never ran and liability recorded neither success nor failure. Bind an `LLMClient` on the agent (or put `"llm"` in the action context). TnsAI 0.14.0 (`TnsAI@093df1d1`, TAN-5693) — ships in Maven Central 0.14.1.

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)

```java
@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.

```java
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:

```java
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) |

```java
// 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.

```java
// 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.

```java
// 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

```java
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

- [Actions vs Tools](actions-vs-tools.md) -- `@ActionSpec` vs `@Tool` vs `BuiltInTool`
- [Roles](roles.md) -- defining roles with `@ActionSpec` annotations
- [Capabilities](capabilities.md) -- reusable body-less `@ActionSpec` contracts via `@Capability` interfaces
- [Tools](../../capabilities/tools/registration.md) -- registering POJO toolkits the LLM can call
- [Advanced Tools](../../capabilities/tools/registration-advanced.md) -- filters, listeners, and dispatcher introspection
- [SPI Reference](../../reference/spi.md) -- SPI interfaces for cross-module extensibility
