Skip to content
tnsaiDocs
GitHub

Roles

A role defines what an agent can do. It has an identity (name, goal and domain) and a set of actions, the @ActionSpec methods the framework discovers on the class. The role also builds the system prompt the agent sends to the model. An agent can hold several roles.

Build a role with RoleBuilder

Use RoleBuilder to define a role inline, for example in tests, scripts, or when the configuration is loaded at runtime. create() takes no arguments, and .name(...) sets the name. The builder also sets the model, resilience, input and output guardrails, and programmatic actions (addAction).

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();
    }
}

A builder role has no mustNever or mustAlways constraints, because those live on annotated action methods. For them, subclass Role.

Write a role class

Subclass Role, implement getIdentity(), and annotate the action methods. @RoleSpec on the class carries the rest of the role's configuration: description, domains, goals, the model (llm), knowledge and config. Safety constraints go on each action.

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;
    }
}

A subclass without its own @RoleSpec inherits the nearest superclass's. A subclass that declares one replaces the whole specification, and fields are not merged.

For how each action type runs, and how to post-process a result with ActionResult, see Actions.

Inspect a role

These accessors read a role's identity, actions and generated prompts. The per-action constraints are on ActionMetadata.

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();   // identity plus every action and its constraints
String minimalPrompt = role.getMinimalPrompt(); // identity only

Two types are named Capability. com.tnsai.capabilities.Capability is the annotation for reusable actions. com.tnsai.models.agent.Capability is a model type, and a role does not need it.