Skip to content
tnsaijava agent framework

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.

An Agent is the top-level orchestrator in TnsAI. It owns an LLM client, one or more roles, a memory store, and an event system. Agents handle the full chat loop: receiving a message, consulting their roles for available actions, calling the LLM, executing tool calls, and returning a response.

Quick Start

The fastest way to create an agent is with AgentBuilder. build() requires the accountability trio — principal, liabilitySink, and authorityScope — in addition to at least one role. Without all three, it throws IllegalStateException (TNS-298). See Accountability.

package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.RecordingLiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.identity.AgentDescriptor;
import com.tnsai.identity.LocalIdentityProvider;
import com.tnsai.llm.LLMClientFactory;
import com.tnsai.roles.RoleBuilder;

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

public final class FundamentalsBuilderExample {
    private FundamentalsBuilderExample() {}

    public static Agent build() {
        var principal = new LocalIdentityProvider().issue(
            AgentDescriptor.builder()
                .agentClass("com.example.AssistantAgent")
                .systemPrompt("You are a helpful assistant.")
                .toolNames(List.of())
                .model("openai:gpt-4o")
                .build());

        return AgentBuilder.create()
            .llm(LLMClientFactory.create("openai", "gpt-4o", 0.7f))
            .role(RoleBuilder.create()
                .name("Assistant")
                .goal("Help users with their questions")
                .build())
            .principal(principal)
            .liabilitySink(new RecordingLiabilitySink())
            .authorityScope(AuthorityScope.unrestricted(Duration.ofHours(1)))
            .build();
    }
}
String response = FundamentalsBuilderExample.build().chat("What is BDI architecture?");

For more control, extend the Agent class directly:

package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.annotations.AgentSpec;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.AnthropicClient;
import com.tnsai.roles.Role;

import java.util.List;

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

    @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 agent = new AnnotatedAgentExample();
        agent.start();
        String answer = agent.chat("What is quantum computing?");
        System.out.println(answer);
    }
}

The agent name is derived from the class (AnnotatedAgentExample"Annotated Agent Example"). @AgentSpec has no name element. A direct Agent subclass must implement getRoles(). Override getLLM() to provide its agent-level client; @AgentSpec.llm is declared annotation metadata but is not consumed by the released 0.14.1 runtime or current framework main.

Creating Agents

There are two ways to create an agent: programmatically with AgentBuilder, or declaratively by extending the Agent class and using annotations. Use the builder when you want quick, inline setup. Use annotations when you want a reusable agent class with its configuration baked in.

With AgentBuilder (programmatic)

AgentBuilder lets you configure an agent in a single fluent chain. This is the best approach for simple agents or when you want to assemble an agent dynamically at runtime.

package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.enums.BuiltInTool;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.providers.OpenAIClient;
import com.tnsai.memory.InMemoryStore;
import com.tnsai.roles.Role;

import java.util.List;

public final class FundamentalsConfigExample {
    private FundamentalsConfigExample() {}

    public static Agent build(
        Role myRole,
        Role role1,
        Role role2,
        Object myDomainTools,
        AgentPrincipal principal,
        LiabilitySink liabilitySink,
        AuthorityScope authorityScope
    ) {
        return AgentBuilder.create()
            .id("agent-001")
            .llm(new OpenAIClient("gpt-4o"))
            .role(myRole)
            .roles(List.of(role1, role2))
            .builtInTools(BuiltInTool.WEB_SEARCH_TOOLS, BuiltInTool.UTILITY_TOOLS)
            .toolPojos(myDomainTools)
            .memoryStore(new InMemoryStore())
            .maxContextTokens(8192)
            .principal(principal)
            .liabilitySink(liabilitySink)
            .authorityScope(authorityScope)
            .build();
    }
}

With Annotations (declarative)

If you prefer a class-per-agent design, extend Agent and use @AgentSpec for metadata and the members the runtime consumes. The compiled example above shows the required template methods. getRoles() is abstract: return the roles directly, or return an empty list to let @AgentSpec.roles supply its fallback role classes. Each annotation-declared role needs a public no-arg constructor. Roles that take constructor arguments still go through AgentBuilder.role(...) or a non-empty getRoles() result.

@LLMSpec is nested-only (@Target({})), so it is not valid directly on an agent class. @RoleSpec(llm = …) consumes it for a role-level client. Although @AgentSpec(llm = …) is syntactically valid, the agent initializer does not read that member today; use getLLM() or AgentBuilder.llm(...) instead.

Chat Methods

Once you have an agent, you interact with it through chat methods. TnsAI provides several variants depending on whether you need conversation history, streaming output, or visibility into tool calls happening inside the agent loop.

// Simple chat — single turn, uses conversation history
String response = agent.chat("Explain quantum computing");

// Chat without history
String response = agent.chat("Translate this to French", false);

// Streaming — returns tokens as they arrive
Stream<String> tokens = agent.streamChat("Write a poem about Java");
tokens.forEach(System.out::print);

// Event-driven chat — full visibility into the agent loop
String response = agent.chatWithEvents("Research AI safety", event -> {
    switch (event) {
        case ToolCallStartEvent e -> System.out.println("Calling: " + e.toolName());
        case ToolCallEndEvent e -> System.out.println("Result: " + e.result());
        case ErrorEvent e -> System.err.println("Error: " + e.message());
        default -> {}
    }
});

Memory Management

Agents automatically track conversation history so the LLM has context across turns. You can also inspect, modify, or prune this history directly when you need to manage token usage or reset a conversation.

// Get conversation history
List<Map<String, Object>> history = agent.getConversationHistory();

// Clear all history
agent.clearConversationHistory();

// Keep the MemoryStore you passed to AgentBuilder.memoryStore(...) —
// Agent.getMemoryStore() is protected, not a public accessor.
memoryStore.addMessage("user", "Remember this context");

// Prune memory to fit within a token limit (removes oldest messages first)
memoryStore.prune(4096);

Lifecycle

Agents have a start/stop lifecycle. start() is a no-op — protocols auto-start when annotations are detected. Call stop() to release resources (there is no shutdown() or isRunning()). Inspect operational health with getHealthState().

agent.start();
AgentHealthState health = agent.getHealthState();
agent.stop();

Configuration Summary

This table lists the common AgentBuilder properties. Required at build(): at least one role plus the accountability trio. .llm(...) is optional for traditional (non-LLM) agents. There is no .model(String), .systemPrompt(...), .guardrail(...), or .ragPipeline(...) on AgentBuilder.

PropertyBuilder MethodDefaultDescription
ID.id(String)Auto-generatedUnique agent identifier
LLM.llm(LLMClient)OptionalLanguage model client (omit for traditional agents)
Roles.role(Role) / .roles(List)RequiredAgent roles
Principal.principal(AgentPrincipal)RequiredActor identity (TNS-298)
Liability sink.liabilitySink(LiabilitySink)RequiredAudit destination
Authority scope.authorityScope(AuthorityScope)RequiredWhat the agent may do
Built-in toolkits.builtInTools(BuiltInTool...)EmptyShipped POJO toolkits from tnsai-tools
Custom toolkits.toolPojos(Object...)EmptyYour own POJOs with @Tool methods
Runtime tools.dynamicTool(DynamicToolMethod)EmptyTools whose identity is only known at runtime (e.g. MCP proxies)
Memory.memoryStore(MemoryStore)InMemoryStoreConversation memory
Context limit.maxContextTokens(int)Provider defaultMax context window
Chat knowledge.chatKnowledge(String)NoneNames one declared source to ground chat; pair with .addKnowledgeSource / .retrieval. .knowledgeBase(KnowledgeBase) was removed in 0.16.0.
Prompt strategy.promptStrategy(PromptStrategy)DefaultPrompt enhancement
Reasoning.reasoningStrategy(String)NoneReasoning strategy name

SPI Extension Points

The Core module defines SPI interfaces that other modules implement. Extensions are discovered automatically via ServiceLoader:

SPI InterfacePurposeImplementing Module
MessageBrokerAgent communication routingCoordination
ResilienceStrategyResilience pattern implementationsQuality
CognitiveModelCognitive processing modelsIntelligence
CheckpointerFactoryState checkpointingCustom
CheckpointerProviderCheckpoint storage backendsCustom

Register an SPI implementation by adding a file to META-INF/services/:

# META-INF/services/com.tnsai.spi.MessageBroker
com.example.MyCustomMessageBroker

Next in this Section

  • Roles — Bundling capabilities into Role classes.
  • Action System@ActionSpec routing and executor types.
  • Actions vs Tools@ActionSpec vs @Tool vs BuiltInTool.
  • Capabilities — Reusable body-less action contracts via @Capability interfaces.
  • Events — The agent's lifecycle event bus.