# Create an agent

Build an agent with `AgentBuilder`, or as an annotated `Agent` subclass, then chat with it.

An `Agent` owns an LLM client, one or more roles, a memory store and an event system. On each chat turn it sends the message and the roles' actions to the model, runs the tool calls the model makes, and returns the reply.

## Build an agent with `AgentBuilder`

`AgentBuilder` assembles an agent in one fluent chain. `build()` needs at least one role, a `principal` and an `authorityScope`. A `liabilitySink` is optional. See [Principal and authority](principal-and-authority.md).

<!-- java-contract: src/main/java/com/example/tnsai/docs/FundamentalsBuilderExample.java -->
```java
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();
    }
}
```

```java
String reply = FundamentalsBuilderExample.build().chat("Summarize the release notes in three bullets");
```

`build()` first runs a validation pass over the configuration. Every error-level finding is collected into one `AgentValidationException`, and warnings are logged. Without a principal or an authority scope, `build()` throws `IllegalStateException`.

A fuller chain sets the id, several roles, toolkits, memory and a context limit:

<!-- java-contract: src/main/java/com/example/tnsai/docs/FundamentalsConfigExample.java -->
```java
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();
    }
}
```

## Write an annotated agent class

Extend `Agent` when you want a reusable class with its configuration on it. `getRoles()` is abstract, and `getLLM()` supplies the agent's client. `AssistantRole` comes from the [quickstart](../start/quickstart.md#option-2-annotated-agent-subclass).

<!-- java-contract: src/main/java/com/example/tnsai/docs/AnnotatedAgentExample.java -->
```java
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);
    }
}
```

A direct subclass does not get accountability wiring from a builder. Pass it an `ActionExecutor` on which `setPrincipal` and `setDefaultScope` have been called, through the four-argument constructor as above. Without them, the agent's first action is refused before its body runs. See [Principal and authority](principal-and-authority.md).

* **Name.** The agent name comes from the class: a trailing `Agent` is dropped and the words are split, so `ResearchAgent` becomes `"Research"`. `@AgentSpec` has no `name` element.
* **Roles.** Return the roles from `getRoles()`, or return an empty list to use the role classes in `@AgentSpec.roles`. Each annotation-declared role needs a public no-arg constructor. A role that takes constructor arguments goes through `getRoles()` or `AgentBuilder.role(...)`.
* **LLM.** `@AgentSpec(llm = …)` compiles but is not read for the agent's client. Use `getLLM()` or `AgentBuilder.llm(...)`. `@LLMSpec` is valid only nested, and `@RoleSpec(llm = …)` gives a role its own client.

### Inherited `@AgentSpec`

`@AgentSpec` is `@Inherited`. An agent class without its own `@AgentSpec` takes its nearest superclass's whole. Fields are not merged: a subclass that declares its own `@AgentSpec` replaces the superclass's entirely. An `@AgentSpec` on an interface is not inherited.

When concrete agents extend an annotated abstract base and declare none, each one takes the base's settings:

* **Knowledge, `@Config` file, roles and groups.** Each agent binds and indexes the inherited knowledge itself, so inherited `DIRECT` knowledge needs a retrieval runtime such as `tnsai-rag` at start. It joins the base's groups on start when `autoJoin` is set, and plays the base's `roles` when given none in code. An inherited `@Config` file's `roles` sections bind the subclass's roles, so a subclass that plays fewer roles than they name fails at start.
* **Memory and communication.** An inherited `memory` with `FILE` persistence writes each agent's history to disk, and `REDIS` or `DATABASE` opens a connection per agent. A `getMemoryStore()` or `getCommunication()` override below the annotated class, or a `@CommunicationStyle` on the agent's own class, replaces the inherited value.
* **DID.** A fixed `agentId` in an inherited `did` fails at start, so declare it on the class's own `@AgentSpec`. An inherited `did` without an `agentId` gives each agent a DID of its own id.
* **A subclass with only `@RoleSpec`** has both annotations. Its knowledge is its role's and the inherited agent's together, so it fails at start when both name a `@Config` file or declare knowledge of the same name.

The other members (`autoStart`, `idleTimeoutMs`, `toolCallFilter`, `maxContextTokens`, description and version) are inherited too. A run file section keyed by the superclass configures only the superclass. See [Configuration files and the run file](../rag/index.md#configuration-files-and-the-run-file).

To opt out, declare an `@AgentSpec` on the subclass.

## Chat with the agent

```java
// One turn with conversation history and tools
String reply = agent.chat("Explain quantum computing");

// Without history
String translated = agent.chat("Translate this to French", false);

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

// Watch tool calls inside the loop
String answer = 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 -> {}
    }
});
```

`chat(message, useHistory, useTools)` also turns tools off for a turn. See [Streaming](streaming.md) and [Events](events.md).

## Manage conversation history

```java
List<Map<String, Object>> history = agent.getConversationHistory();
agent.clearConversationHistory();
```

`Agent.getMemoryStore()` is protected. To add messages or prune, keep the `MemoryStore` you passed to `AgentBuilder.memoryStore(...)`:

```java
memoryStore.addMessage("user", "Remember this context");
memoryStore.prune(4096); // drops the oldest messages first
```

See [Memory](memory.md).

## Start and stop

`start()` does nothing, because protocols start when their annotations are detected. `stop()` releases the agent's resources and is safe to call twice. `getHealthState()` returns the agent's `AgentHealthState`.

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

## Builder settings

| Setting | Builder method | Default | Description |
| --- | --- | --- | --- |
| ID | `.id(String)` | Generated | Unique agent identifier |
| LLM | `.llm(LLMClient)` | None | Model client. Omit it for an agent without an LLM |
| Roles | `.role(Role)` / `.roles(List)` | Required | The agent's roles |
| Principal | `.principal(AgentPrincipal)` | Required | Who the agent acts as |
| Authority scope | `.authorityScope(AuthorityScope)` | Required | What the agent may do |
| Liability sink | `.liabilitySink(LiabilitySink)` | None | Where liability records go. None are built without one |
| Description | `.description(String)` | None | Agent description |
| Built-in toolkits | `.builtInTools(BuiltInTool...)` | None | Toolkits shipped in `tnsai-tools` |
| Custom toolkits | `.toolPojos(Object...)` | None | Your objects with `@Tool` methods |
| Runtime tools | `.dynamicTool(DynamicToolMethod)` | None | Tools known only at runtime, such as MCP proxies |
| Tool call filter | `.toolCallFilter(ToolCallFilter)` | None | Consulted before each tool call. Validation warns when a confirmation-required tool has no filter |
| Memory | `.memoryStore(MemoryStore)` | `InMemoryStore` | Conversation memory |
| Context limit | `.maxContextTokens(int)` | Provider default | Maximum context window |
| Idle timeout | `.idleTimeoutMs(int)` | `0` | Suspends an idle agent. `0` never suspends |
| Knowledge | `.knowledge(KnowledgeEntry...)` | None | Replaces annotation entries of the same name; other entries join chat. `.actionKnowledge(action, names...)` sets one action's entries |
| Reasoning | `.reasoningStrategy(String)` | None | A named reasoning strategy from `tnsai-intelligence`. Ignored when the module is absent |
| Tenant | `.tenantId(String)` | None | Binds each public chat turn to `TenantContext` |
| Validation | `.relaxValidation(String)` / `(Set<String>)` | None | Turns off a validation check by its code |

## Related

- [Principal and authority](principal-and-authority.md): the identity and scope `build()` requires.
- [Roles](roles.md): group actions into roles.
- [Actions](actions.md): what an agent can do.
- [Events](events.md): the agent's event stream.
- [SPI reference](../reference/spi.md): extension points loaded from the classpath.
