Skip to content
tnsaiDocs
GitHub

Quickstart

Build your first TnsAI agent in 5 minutes. Prerequisites: Installation.

Your First Agent

Option 1: AgentBuilder (no subclassing)

package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.RecordingLiabilitySink;
import com.tnsai.actions.ActionDiscovery;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.enums.ActionType;
import com.tnsai.identity.AgentDescriptor;
import com.tnsai.identity.LocalIdentityProvider;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

import java.time.Duration;
import java.util.List;

public final class QuickstartBuilderExample {

    private QuickstartBuilderExample() {
    }

    public static final class AssistantRole extends Role {
        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity(
                    "assistant",
                    "Friendly conversational agent",
                    "support");
        }

        @ActionSpec(
                type = ActionType.LOCAL,
                description = "Greet the user by name")
        public String greet(String name) {
            return "Hello, " + name + "!";
        }
    }

    public static Agent buildAgent() {
        String model = "claude-sonnet-4-20250514";
        var role = Role.create(AssistantRole.class);
        var actionNames = ActionDiscovery.discoverActions(role).getActions().stream()
                .map(action -> action.getName())
                .toList();
        var principal = new LocalIdentityProvider().issue(
                AgentDescriptor.builder()
                        .agentClass(AssistantRole.class.getName())
                        .systemPrompt("You are a friendly assistant.")
                        .toolNames(actionNames)
                        .model("anthropic:" + model)
                        .build());

        return AgentBuilder.create()
                .llm(new AnthropicClient(model))
                .role(role)
                .principal(principal)
                .liabilitySink(new RecordingLiabilitySink())
                .authorityScope(AuthorityScope.unrestricted(Duration.ofHours(1)))
                .build();
    }

    public static void main(String[] args) {
        Agent agent = buildAgent();
        agent.start();
        String response = agent.chat("Hi, my name is Alice");
        System.out.println(response);
    }
}

Required. The AgentBuilder path needs principal and authorityScope. Without both, build() throws IllegalStateException, and the framework ships no silent no-op default. liabilitySink is optional. Without one, the framework constructs and retains no AgentLiabilityRecord at all. RecordingLiabilitySink is in-memory, for tests. The framework ships no filesystem writer either, so production audit trails need your own LiabilitySink implementation. See Audit agent actions for the full identity / liability / scope model.

Option 2: Annotated Agent (subclass)

package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.actions.ActionExecutor;
import com.tnsai.agents.Agent;
import com.tnsai.annotations.AgentSpec;
import com.tnsai.identity.AgentDescriptor;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.identity.LocalIdentityProvider;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.roles.Role;

import java.time.Duration;
import java.util.List;

@AgentSpec(description = "A helpful research assistant")
public final class AnnotatedAgentExample extends Agent {

    public AnnotatedAgentExample(AgentPrincipal principal, AuthorityScope scope) {
        super("annotated-agent", null, null, wired(principal, scope));
    }

    private static ActionExecutor wired(AgentPrincipal principal, AuthorityScope scope) {
        ActionExecutor executor = new ActionExecutor();
        executor.setPrincipal(principal);
        executor.setDefaultScope(scope);
        return executor;
    }

    @Override
    protected LLMClient getLLM() {
        return new AnthropicClient("claude-sonnet-4-20250514");
    }

    @Override
    protected List<Role> getRoles() {
        return List.of(Role.create(QuickstartBuilderExample.AssistantRole.class));
    }

    public static void main(String[] args) {
        var principal = new LocalIdentityProvider().issue(
                AgentDescriptor.builder()
                        .agentClass(AnnotatedAgentExample.class.getName())
                        .systemPrompt("You are a helpful research assistant.")
                        .toolNames(List.of("greet"))
                        .model("anthropic:claude-sonnet-4-20250514")
                        .build());
        var agent = new AnnotatedAgentExample(
                principal, AuthorityScope.unrestricted(Duration.ofHours(1)));
        agent.start();
        String answer = agent.chat("What is quantum computing?");
        System.out.println(answer);
    }
}

agent.start() in both examples does nothing: its body is empty, and you can leave the call out.

Wire the executor. AgentBuilder wires accountability for you, but a direct Agent subclass does not. Pass an ActionExecutor on which setPrincipal and setDefaultScope have been called through the four-argument constructor, as above. Without them the agent's first role action is refused before its body runs, and a chat with it ends on that tool call.

Adding Tools

For brevity the examples below omit the .principal(...), .liabilitySink(...), and .authorityScope(...) calls. Assume the principal, sink, and scope from Option 1. Every AgentBuilder.build() still requires principal and authorityScope. liabilitySink is optional but assumed here too.

Register shipped POJO toolkits by enum constant, which is compile-safe:

import com.tnsai.enums.BuiltInTool;

var agent = AgentBuilder.create()
    .llm(new AnthropicClient("claude-sonnet-4-20250514"))
    .role(Role.create(AssistantRole.class))
    .builtInTools(
        BuiltInTool.WEB_SEARCH_TOOLS,   // brave_search, duckduckgo, wikipedia, …
        BuiltInTool.PDF_TOOLS,          // pdf_extract_text, pdf_metadata, …
        BuiltInTool.MARKDOWN_TOOLS      // markitdown
    )
    .build();

Each enum entry registers every @Tool-annotated method on the backing POJO. The LLM sees them all (e.g. brave_search, pdf_extract_text, markitdown) and picks one per call.

For your own toolkits, register the POJO instances directly:

var agent = AgentBuilder.create()
    .llm(llm)
    .role(role)
    .toolPojos(new MyDomainTools(), new MyAnalyticsTools())
    .build();

See Tools / Catalog for the full shipped catalog and Custom Tools for the @Tool annotation pattern.

Streaming

agent.streamChatWithTools("Summarize this paper", chunk -> {
    if (chunk.isContent()) {
        System.out.print(chunk.getContent());
    }
});

Structured Output

record WeatherInfo(String city, double temperature, String condition) {}

WeatherInfo weather = agent.chatWithFormat(
    "What's the weather in Istanbul?",
    WeatherInfo.class,
    3 // max retries
);

Next step

Build a complete agent in the research agent tutorial.