TnsAI
Core

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.

Creating Roles

There are two ways to create a role: programmatically with RoleBuilder, or declaratively with annotations. Pick whichever style fits your project -- they produce the same result.

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.

Role role = RoleBuilder.create()
    .name("Researcher")
    .goal("Find and synthesize information from academic sources")
    .domain("academic-research")
    .duty("Search papers", "Users need access to recent research")
    .duty("Summarize findings")
    .mustNever("Fabricate citations", "Academic integrity")
    .mustAlways("Include source references", "Traceability")
    .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 everything -- name, capabilities, LLM config -- is visible at a glance.

@RoleSpec(
    name = "Researcher",
    description = "Finds and synthesizes academic information",
    beliefs = {"research_context", "available_sources"},
    desires = {"find_papers", "synthesize_findings"},
    intentions = {"search_database", "analyze_paper"},
    capabilities = {"search", "analysis", "summarization"},
    domains = {"academic", "research"},
    responsibilities = {
        @Responsibility(
            name = "Paper Search",
            description = "Search academic databases",
            actions = {"searchPapers", "filterResults"}
        )
    },
    llm = @LLMConfig(provider = "anthropic", model = "claude-sonnet-4-20250514")
)
public class ResearchRole extends Role {

    @Override
    public RoleIdentity getIdentity() {
        return new RoleIdentity("Researcher", "Find papers", "academic");
    }

    @Override
    public List<Responsibility> getResponsibilities() {
        return List.of(
            new CoreDuty("Search papers", "Find relevant research"),
            new CoreDuty("Analyze findings", "Extract key insights")
        );
    }
}

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:

TypeExecutorDescription
LOCALTypedActionExecutorDirect method invocation via reflection
WEB_SERVICEWebServiceExecutorHTTP REST API calls
LLMLLMToolsExecutorLLM with tool/function calling
MCP_TOOLMcpToolExecutorModel 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(
    name = "searchPapers",
    description = "Search for academic papers on a topic",
    type = ActionType.LLM
)
public String searchPapers(@LLMParam("The search query") String query) {
    // Implementation
}

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,
    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,
    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.

MethodReturn typeDescription
getValue()ObjectRaw result value
asString()StringValue 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()JsonNodeJackson JsonNode for flexible JSON traversal
as(Class<T>)TDeserialize to a specific type
isNull()booleanTrue if the underlying value is null
isEmpty()booleanTrue 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,
    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, safety constraints, and generated system prompt. This is useful for debugging, logging, or building tooling around roles.

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");

List<SafetyProperty> mustNever = role.getMustNeverConstraints();
List<SafetyProperty> mustAlways = role.getMustAlwaysConstraints();

String systemPrompt = role.getSystemPrompt();
String minimalPrompt = role.getMinimalPrompt();

BDI Model

TnsAI implements the Belief-Desire-Intention (BDI) architecture for agent reasoning:

Agent agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .identity(new AgentIdentity("ResearchBot", "AI Research Assistant"))
    .belief(new Belief("domain", "artificial intelligence"))
    .belief(new Belief("max_papers", 10))
    .desire(new Desire("find_papers", "Locate relevant research papers"))
    .desire(new Desire("synthesize", "Create comprehensive summaries"))
    .intention(new Intention("search", "Search academic databases"))
    .intention(new Intention("analyze", "Analyze paper contents"))
    .capability(new Capability("search", "Academic database search"))
    .capability(new Capability("summarization", "Text summarization"))
    .build();
ConceptClassPurpose
BeliefBeliefWhat the agent knows (key-value pairs)
DesireDesireWhat the agent wants to achieve (goals)
IntentionIntentionHow the agent plans to act (committed plans)
CapabilityCapabilityWhat the agent can do (skills)
PlanPlanStructured plan for achieving desires

On this page