Roles
A Role defines what an agent can do. Each role has an identity (name, goal, domain) and discoverable actions. Safety constraints live on those actions (@ActionSpec.mustNever / mustAlways), not on the role itself. 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.
Creating Roles
There are two ways to create a role: programmatically with RoleBuilder, or by subclassing Role (optionally with @RoleSpec). RoleBuilder sets identity, LLM, guardrails, and programmatic actions. Builder-built roles do not carry safety constraints — for mustNever / mustAlways behavior, subclass Role and annotate the action methods.
With RoleBuilder (programmatic)
Use RoleBuilder when you want to define a role inline -- for example in tests, scripts, or when the role configuration is loaded dynamically at runtime. create() takes no arguments; set the name with .name(...).
package com.example.tnsai.docs;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.roles.Role;
import com.tnsai.roles.RoleBuilder;
public final class RolesBuilderExample {
private RolesBuilderExample() {}
public static Role createResearcher() {
return RoleBuilder.create()
.name("Researcher")
.goal("Find and synthesize information from academic sources")
.domain("academic-research")
.llm(new AnthropicClient("claude-sonnet-4-20250514"))
.build();
}
}With Annotations (declarative)
Use @RoleSpec when you want the role definition to live directly on the class. This is the preferred approach for production roles because identity, LLM config, and per-action safety constraints are visible at a glance.
package com.example.tnsai.docs;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.Goal;
import com.tnsai.annotations.LLMSpec;
import com.tnsai.annotations.RoleSpec;
import com.tnsai.enums.ActionType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;
@RoleSpec(
name = "Researcher",
description = "Finds and synthesizes academic information",
domains = {"academic", "research"},
goals = {
@Goal(
name = "find_papers",
condition = "relevant_papers_found",
description = "Locate and synthesize relevant research"
)
},
llm = @LLMSpec(provider = LLMSpec.Provider.ANTHROPIC, model = "claude-sonnet-4-20250514")
)
public final class RolesSpecExample extends Role {
@Override
public RoleIdentity getIdentity() {
return new RoleIdentity("Researcher", "Find papers", "academic");
}
@ActionSpec(
type = ActionType.LOCAL,
description = "Search for academic papers on a topic",
mustNever = {"fabricate references", "plagiarize content"},
mustAlways = {"cite sources properly"}
)
public String searchPapers(String query) {
return query;
}
}Actions
An Action is a method annotated with @ActionSpec on a Role class. Actions are routed to one of four executor types based on their ActionType:
| Type | Executor | Description |
|---|---|---|
LOCAL | TypedActionExecutor | Direct method invocation via reflection |
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 tools |
Defining Actions
Annotate any method on your Role class with @ActionSpec to expose it as an action. The type field tells the framework which executor handles the call.
@ActionSpec(
description = "Search for academic papers on a topic",
type = ActionType.LLM
)
public String searchPapers(String query) {
return query;
}ActionResult
When an action delegates to an external system (HTTP call, LLM tool, MCP tool), the framework executes the call and makes the raw result available as an ActionResult. There are two usage patterns:
Pure Delegate (Abstract, No Body)
If the method has no body (abstract or the framework handles it entirely), the framework executes the action and returns the result directly. No ActionResult parameter is needed:
@ActionSpec(
type = ActionType.WEB_SERVICE,
description = "Fetch a user by id",
webService = @WebService(endpoint = "https://api.example.com/users/{id}")
)
public abstract Object getUser(String id);Post-Process with ActionResult
Add an ActionResult parameter to receive the raw execution result and transform it before returning:
@ActionSpec(
type = ActionType.WEB_SERVICE,
description = "Fetch a record by id",
webService = @WebService(endpoint = "https://api.example.com/data/{id}")
)
public Object getData(String id, ActionResult result) {
// Return as-is
return result;
// Or extract a field
Map<String, Object> json = result.asMap();
return json.get("name");
}ActionResult API
ActionResult wraps the raw value returned by the external system and provides convenience methods for common conversions like JSON parsing and type deserialization.
| Method | Return type | Description |
|---|---|---|
getValue() | Object | Raw result value |
asString() | String | Value as String (JSON-serialized if not already a String) |
asMap() | Map<String, Object> | Value as Map (parsed from JSON if needed) |
asList() | List<Object> | Value as List (parsed from JSON if needed) |
asJson() | JsonNode | Jackson JsonNode for flexible JSON traversal |
as(Class<T>) | T | Deserialize to a specific type |
isNull() | boolean | True if the underlying value is null |
isEmpty() | boolean | True if null, empty String, empty Map, or empty List |
Example -- Transforming a Web Service Response
This example shows a common pattern: calling a weather API and reshaping the JSON response into a human-readable string before returning it to the agent.
@ActionSpec(
type = ActionType.WEB_SERVICE,
description = "Fetch a weather forecast",
webService = @WebService(endpoint = "https://api.weather.com/forecast/{city}")
)
public String getForecast(String city, ActionResult result) {
JsonNode json = result.asJson();
String temp = json.path("main").path("temp").asText();
String desc = json.path("weather").get(0).path("description").asText();
return String.format("Temperature: %s, Conditions: %s", temp, desc);
}Role Accessors
Once you have a Role instance, these methods let you inspect its identity, actions, and generated system prompt. Per-action safety constraints are on ActionMetadata, not on the role.
RoleIdentity identity = role.identity();
String name = role.getName();
String goal = role.getGoal();
String domain = role.getDomain();
List<ActionMetadata> actions = role.getActions();
int count = role.getActionCount();
boolean has = role.hasAction("searchPapers");
Optional<ActionMetadata> action = role.getAction("searchPapers");
String[] mustNever = action.map(ActionMetadata::getMustNever).orElse(new String[0]);
String[] mustAlways = action.map(ActionMetadata::getMustAlways).orElse(new String[0]);
String systemPrompt = role.getSystemPrompt();
String minimalPrompt = role.getMinimalPrompt();BDI Model
Belief, Desire, Intention, and Capability remain as model types. They
are not seeded on AgentBuilder. The fluent methods .belief(...),
.desire(...), .intention(...), and .capability(...) were removed in
0.13.0 (TNS-541). Agent has no getBeliefs() / getDesires() /
getIntentions(). The chat loop is LLM + tools, not a BDI interpreter.
AgentBuilder.identity(AgentIdentity) still exists. Optional BDI planning
goes through the CognitiveModel.bdi() SPI — see SPI Reference.
Agent agent = AgentBuilder.create()
.llm(llm)
.role(role)
.identity(new AgentIdentity("ResearchBot", "AI Research Assistant"))
.principal(principal)
.liabilitySink(sink)
.authorityScope(scope)
.build();| Concept | Class | Purpose |
|---|---|---|
| Belief | Belief | What the agent knows (key-value pairs) |
| Desire | Desire | What the agent wants to achieve (goals) |
| Intention | Intention | How the agent plans to act (committed plans) |
| Capability | Capability | What the agent can do (skills) |
| Plan | Plan | Structured plan for achieving desires |
Fundamentals
The core moving parts of a single agent. This page covers the Agent class itself — construction, chat, memory, lifecycle. See the other pages in this section for Roles, the Action System, Capabilities, and Events.
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.