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.
| Property | Builder Method | Default | Description |
|---|---|---|---|
| ID | .id(String) | Auto-generated | Unique agent identifier |
| LLM | .llm(LLMClient) | Optional | Language model client (omit for traditional agents) |
| Roles | .role(Role) / .roles(List) | Required | Agent roles |
| Principal | .principal(AgentPrincipal) | Required | Actor identity (TNS-298) |
| Liability sink | .liabilitySink(LiabilitySink) | Required | Audit destination |
| Authority scope | .authorityScope(AuthorityScope) | Required | What the agent may do |
| Built-in toolkits | .builtInTools(BuiltInTool...) | Empty | Shipped POJO toolkits from tnsai-tools |
| Custom toolkits | .toolPojos(Object...) | Empty | Your own POJOs with @Tool methods |
| Runtime tools | .dynamicTool(DynamicToolMethod) | Empty | Tools whose identity is only known at runtime (e.g. MCP proxies) |
| Memory | .memoryStore(MemoryStore) | InMemoryStore | Conversation memory |
| Context limit | .maxContextTokens(int) | Provider default | Max context window |
| Chat knowledge | .chatKnowledge(String) | None | Names one declared source to ground chat; pair with .addKnowledgeSource / .retrieval. .knowledgeBase(KnowledgeBase) was removed in 0.16.0. |
| Prompt strategy | .promptStrategy(PromptStrategy) | Default | Prompt enhancement |
| Reasoning | .reasoningStrategy(String) | None | Reasoning strategy name |
SPI Extension Points
The Core module defines SPI interfaces that other modules implement. Extensions are discovered automatically via ServiceLoader:
| SPI Interface | Purpose | Implementing Module |
|---|---|---|
MessageBroker | Agent communication routing | Coordination |
ResilienceStrategy | Resilience pattern implementations | Quality |
CognitiveModel | Cognitive processing models | Intelligence |
CheckpointerFactory | State checkpointing | Custom |
CheckpointerProvider | Checkpoint storage backends | Custom |
Register an SPI implementation by adding a file to META-INF/services/:
# META-INF/services/com.tnsai.spi.MessageBroker
com.example.MyCustomMessageBrokerNext in this Section
- Roles — Bundling capabilities into
Roleclasses. - Action System —
@ActionSpecrouting and executor types. - Actions vs Tools —
@ActionSpecvs@ToolvsBuiltInTool. - Capabilities — Reusable body-less action contracts via
@Capabilityinterfaces. - Events — The agent's lifecycle event bus.
Agents
Everything about building a single agent — from the first Agent instance to advanced cognitive composition.
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.